// Copyright (c) 2010 Satoshi Nakamoto
// Copyright (c) 2009-2019 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <rpc/blockchain.h>

#include <avalanche/processor.h>
#include <blockfilter.h>
#include <chain.h>
#include <chainparams.h>
#include <clientversion.h>
#include <coins.h>
#include <common/args.h>
#include <config.h>
#include <consensus/amount.h>
#include <consensus/params.h>
#include <consensus/validation.h>
#include <core_io.h>
#include <hash.h>
#include <index/blockfilterindex.h>
#include <index/coinstatsindex.h>
#include <logging/timer.h>
#include <net.h>
#include <net_processing.h>
#include <node/blockstorage.h>
#include <node/coinstats.h>
#include <node/context.h>
#include <node/utxo_snapshot.h>
#include <primitives/transaction.h>
#include <rpc/server.h>
#include <rpc/server_util.h>
#include <rpc/util.h>
#include <script/descriptor.h>
#include <serialize.h>
#include <streams.h>
#include <txdb.h>
#include <txmempool.h>
#include <undo.h>
#include <util/check.h>
#include <util/fs.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <util/translation.h>
#include <validation.h>
#include <validationinterface.h>
#include <warnings.h>

#include <condition_variable>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>

using kernel::CCoinsStats;
using kernel::CoinStatsHashType;

using node::BlockManager;
using node::GetUTXOStats;
using node::NodeContext;
using node::SnapshotMetadata;

struct CUpdatedBlock {
    BlockHash hash;
    int height;
};

static GlobalMutex cs_blockchange;
static std::condition_variable cond_blockchange;
static CUpdatedBlock latestblock GUARDED_BY(cs_blockchange);

std::tuple<std::unique_ptr<CCoinsViewCursor>, CCoinsStats, const CBlockIndex *>
PrepareUTXOSnapshot(Chainstate &chainstate,
                    const std::function<void()> &interruption_point = {})
    EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

UniValue
WriteUTXOSnapshot(Chainstate &chainstate, CCoinsViewCursor *pcursor,
                  CCoinsStats *maybe_stats, const CBlockIndex *tip,
                  AutoFile &afile, const fs::path &path,
                  const fs::path &temppath,
                  const std::function<void()> &interruption_point = {});

/**
 * Calculate the difficulty for a given block index.
 */
double GetDifficulty(const CBlockIndex &blockindex) {
    int nShift = (blockindex.nBits >> 24) & 0xff;
    double dDiff = double(0x0000ffff) / double(blockindex.nBits & 0x00ffffff);

    while (nShift < 29) {
        dDiff *= 256.0;
        nShift++;
    }
    while (nShift > 29) {
        dDiff /= 256.0;
        nShift--;
    }

    return dDiff;
}

static int ComputeNextBlockAndDepth(const CBlockIndex &tip,
                                    const CBlockIndex &blockindex,
                                    const CBlockIndex *&next) {
    next = tip.GetAncestor(blockindex.nHeight + 1);
    if (next && next->pprev == &blockindex) {
        return tip.nHeight - blockindex.nHeight + 1;
    }
    next = nullptr;
    return &blockindex == &tip ? 1 : -1;
}

static const CBlockIndex *ParseHashOrHeight(const UniValue &param,
                                            ChainstateManager &chainman) {
    LOCK(::cs_main);
    CChain &active_chain = chainman.ActiveChain();

    if (param.isNum()) {
        const int height{param.getInt<int>()};
        if (height < 0) {
            throw JSONRPCError(
                RPC_INVALID_PARAMETER,
                strprintf("Target block height %d is negative", height));
        }
        const int current_tip{active_chain.Height()};
        if (height > current_tip) {
            throw JSONRPCError(
                RPC_INVALID_PARAMETER,
                strprintf("Target block height %d after current tip %d", height,
                          current_tip));
        }

        return active_chain[height];
    } else {
        const BlockHash hash{ParseHashV(param, "hash_or_height")};
        const CBlockIndex *pindex = chainman.m_blockman.LookupBlockIndex(hash);

        if (!pindex) {
            throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
        }

        return pindex;
    }
}
UniValue blockheaderToJSON(const CBlockIndex &tip,
                           const CBlockIndex &blockindex) {
    // Serialize passed information without accessing chain state of the active
    // chain!
    // For performance reasons
    AssertLockNotHeld(cs_main);

    UniValue result(UniValue::VOBJ);
    result.pushKV("hash", blockindex.GetBlockHash().GetHex());
    const CBlockIndex *pnext;
    int confirmations = ComputeNextBlockAndDepth(tip, blockindex, pnext);
    result.pushKV("confirmations", confirmations);
    result.pushKV("height", blockindex.nHeight);
    result.pushKV("version", blockindex.nVersion);
    result.pushKV("versionHex", strprintf("%08x", blockindex.nVersion));
    result.pushKV("merkleroot", blockindex.hashMerkleRoot.GetHex());
    result.pushKV("time", blockindex.nTime);
    result.pushKV("mediantime", blockindex.GetMedianTimePast());
    result.pushKV("nonce", blockindex.nNonce);
    result.pushKV("bits", strprintf("%08x", blockindex.nBits));
    result.pushKV("difficulty", GetDifficulty(blockindex));
    result.pushKV("chainwork", blockindex.nChainWork.GetHex());
    result.pushKV("nTx", blockindex.nTx);

    if (blockindex.pprev) {
        result.pushKV("previousblockhash",
                      blockindex.pprev->GetBlockHash().GetHex());
    }
    if (pnext) {
        result.pushKV("nextblockhash", pnext->GetBlockHash().GetHex());
    }
    return result;
}

UniValue blockToJSON(BlockManager &blockman, const CBlock &block,
                     const CBlockIndex &tip, const CBlockIndex &blockindex,
                     TxVerbosity verbosity) {
    UniValue result = blockheaderToJSON(tip, blockindex);

    result.pushKV("size", (int)::GetSerializeSize(block, PROTOCOL_VERSION));
    UniValue txs(UniValue::VARR);
    switch (verbosity) {
        case TxVerbosity::SHOW_TXID:
            for (const CTransactionRef &tx : block.vtx) {
                txs.push_back(tx->GetId().GetHex());
            }
            break;

        case TxVerbosity::SHOW_DETAILS:
        case TxVerbosity::SHOW_DETAILS_AND_PREVOUT:
            CBlockUndo blockUndo;
            const bool is_not_pruned{WITH_LOCK(
                ::cs_main, return !blockman.IsBlockPruned(blockindex))};
            const bool have_undo{is_not_pruned && blockman.UndoReadFromDisk(
                                                      blockUndo, blockindex)};
            for (size_t i = 0; i < block.vtx.size(); ++i) {
                const CTransactionRef &tx = block.vtx.at(i);
                // coinbase transaction (i == 0) doesn't have undo data
                const CTxUndo *txundo = (have_undo && i > 0)
                                            ? &blockUndo.vtxundo.at(i - 1)
                                            : nullptr;
                UniValue objTx(UniValue::VOBJ);
                TxToUniv(*tx, BlockHash(), objTx, true, txundo, verbosity);
                txs.push_back(objTx);
            }
            break;
    }

    result.pushKV("tx", txs);

    return result;
}

static RPCHelpMan getblockcount() {
    return RPCHelpMan{
        "getblockcount",
        "Returns the height of the most-work fully-validated chain.\n"
        "The genesis block has height 0.\n",
        {},
        RPCResult{RPCResult::Type::NUM, "", "The current block count"},
        RPCExamples{HelpExampleCli("getblockcount", "") +
                    HelpExampleRpc("getblockcount", "")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            LOCK(cs_main);
            return chainman.ActiveHeight();
        },
    };
}

static RPCHelpMan getbestblockhash() {
    return RPCHelpMan{
        "getbestblockhash",
        "Returns the hash of the best (tip) block in the "
        "most-work fully-validated chain.\n",
        {},
        RPCResult{RPCResult::Type::STR_HEX, "", "the block hash, hex-encoded"},
        RPCExamples{HelpExampleCli("getbestblockhash", "") +
                    HelpExampleRpc("getbestblockhash", "")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            LOCK(cs_main);
            return chainman.ActiveTip()->GetBlockHash().GetHex();
        },
    };
}

void RPCNotifyBlockChange(const CBlockIndex *pindex) {
    if (pindex) {
        LOCK(cs_blockchange);
        latestblock.hash = pindex->GetBlockHash();
        latestblock.height = pindex->nHeight;
    }
    cond_blockchange.notify_all();
}

static RPCHelpMan waitfornewblock() {
    return RPCHelpMan{
        "waitfornewblock",
        "Waits for a specific new block and returns useful info about it.\n"
        "\nReturns the current block on timeout or exit.\n",
        {
            {"timeout", RPCArg::Type::NUM, RPCArg::Default{0},
             "Time in milliseconds to wait for a response. 0 indicates no "
             "timeout."},
        },
        RPCResult{RPCResult::Type::OBJ,
                  "",
                  "",
                  {
                      {RPCResult::Type::STR_HEX, "hash", "The blockhash"},
                      {RPCResult::Type::NUM, "height", "Block height"},
                  }},
        RPCExamples{HelpExampleCli("waitfornewblock", "1000") +
                    HelpExampleRpc("waitfornewblock", "1000")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            int timeout = 0;
            if (!request.params[0].isNull()) {
                timeout = request.params[0].getInt<int>();
            }

            CUpdatedBlock block;
            {
                WAIT_LOCK(cs_blockchange, lock);
                block = latestblock;
                if (timeout) {
                    cond_blockchange.wait_for(
                        lock, std::chrono::milliseconds(timeout),
                        [&block]() EXCLUSIVE_LOCKS_REQUIRED(cs_blockchange) {
                            return latestblock.height != block.height ||
                                   latestblock.hash != block.hash ||
                                   !IsRPCRunning();
                        });
                } else {
                    cond_blockchange.wait(
                        lock,
                        [&block]() EXCLUSIVE_LOCKS_REQUIRED(cs_blockchange) {
                            return latestblock.height != block.height ||
                                   latestblock.hash != block.hash ||
                                   !IsRPCRunning();
                        });
                }
                block = latestblock;
            }
            UniValue ret(UniValue::VOBJ);
            ret.pushKV("hash", block.hash.GetHex());
            ret.pushKV("height", block.height);
            return ret;
        },
    };
}

static RPCHelpMan waitforblock() {
    return RPCHelpMan{
        "waitforblock",
        "Waits for a specific new block and returns useful info about it.\n"
        "\nReturns the current block on timeout or exit.\n",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "Block hash to wait for."},
            {"timeout", RPCArg::Type::NUM, RPCArg::Default{0},
             "Time in milliseconds to wait for a response. 0 indicates no "
             "timeout."},
        },
        RPCResult{RPCResult::Type::OBJ,
                  "",
                  "",
                  {
                      {RPCResult::Type::STR_HEX, "hash", "The blockhash"},
                      {RPCResult::Type::NUM, "height", "Block height"},
                  }},
        RPCExamples{HelpExampleCli("waitforblock",
                                   "\"0000000000079f8ef3d2c688c244eb7a4570b24c9"
                                   "ed7b4a8c619eb02596f8862\" 1000") +
                    HelpExampleRpc("waitforblock",
                                   "\"0000000000079f8ef3d2c688c244eb7a4570b24c9"
                                   "ed7b4a8c619eb02596f8862\", 1000")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            int timeout = 0;

            BlockHash hash(ParseHashV(request.params[0], "blockhash"));

            if (!request.params[1].isNull()) {
                timeout = request.params[1].getInt<int>();
            }

            CUpdatedBlock block;
            {
                WAIT_LOCK(cs_blockchange, lock);
                if (timeout) {
                    cond_blockchange.wait_for(
                        lock, std::chrono::milliseconds(timeout),
                        [&hash]() EXCLUSIVE_LOCKS_REQUIRED(cs_blockchange) {
                            return latestblock.hash == hash || !IsRPCRunning();
                        });
                } else {
                    cond_blockchange.wait(
                        lock,
                        [&hash]() EXCLUSIVE_LOCKS_REQUIRED(cs_blockchange) {
                            return latestblock.hash == hash || !IsRPCRunning();
                        });
                }
                block = latestblock;
            }

            UniValue ret(UniValue::VOBJ);
            ret.pushKV("hash", block.hash.GetHex());
            ret.pushKV("height", block.height);
            return ret;
        },
    };
}

static RPCHelpMan waitforblockheight() {
    return RPCHelpMan{
        "waitforblockheight",
        "Waits for (at least) block height and returns the height and "
        "hash\nof the current tip.\n"
        "\nReturns the current block on timeout or exit.\n",
        {
            {"height", RPCArg::Type::NUM, RPCArg::Optional::NO,
             "Block height to wait for."},
            {"timeout", RPCArg::Type::NUM, RPCArg::Default{0},
             "Time in milliseconds to wait for a response. 0 indicates no "
             "timeout."},
        },
        RPCResult{RPCResult::Type::OBJ,
                  "",
                  "",
                  {
                      {RPCResult::Type::STR_HEX, "hash", "The blockhash"},
                      {RPCResult::Type::NUM, "height", "Block height"},
                  }},
        RPCExamples{HelpExampleCli("waitforblockheight", "100 1000") +
                    HelpExampleRpc("waitforblockheight", "100, 1000")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            int timeout = 0;

            int height = request.params[0].getInt<int>();

            if (!request.params[1].isNull()) {
                timeout = request.params[1].getInt<int>();
            }

            CUpdatedBlock block;
            {
                WAIT_LOCK(cs_blockchange, lock);
                if (timeout) {
                    cond_blockchange.wait_for(
                        lock, std::chrono::milliseconds(timeout),
                        [&height]() EXCLUSIVE_LOCKS_REQUIRED(cs_blockchange) {
                            return latestblock.height >= height ||
                                   !IsRPCRunning();
                        });
                } else {
                    cond_blockchange.wait(
                        lock,
                        [&height]() EXCLUSIVE_LOCKS_REQUIRED(cs_blockchange) {
                            return latestblock.height >= height ||
                                   !IsRPCRunning();
                        });
                }
                block = latestblock;
            }
            UniValue ret(UniValue::VOBJ);
            ret.pushKV("hash", block.hash.GetHex());
            ret.pushKV("height", block.height);
            return ret;
        },
    };
}

static RPCHelpMan syncwithvalidationinterfacequeue() {
    return RPCHelpMan{
        "syncwithvalidationinterfacequeue",
        "Waits for the validation interface queue to catch up on everything "
        "that was there when we entered this function.\n",
        {},
        RPCResult{RPCResult::Type::NONE, "", ""},
        RPCExamples{HelpExampleCli("syncwithvalidationinterfacequeue", "") +
                    HelpExampleRpc("syncwithvalidationinterfacequeue", "")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            SyncWithValidationInterfaceQueue();
            return NullUniValue;
        },
    };
}

static RPCHelpMan getdifficulty() {
    return RPCHelpMan{
        "getdifficulty",
        "Returns the proof-of-work difficulty as a multiple of the minimum "
        "difficulty.\n",
        {},
        RPCResult{RPCResult::Type::NUM, "",
                  "the proof-of-work difficulty as a multiple of the minimum "
                  "difficulty."},
        RPCExamples{HelpExampleCli("getdifficulty", "") +
                    HelpExampleRpc("getdifficulty", "")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            LOCK(cs_main);
            return GetDifficulty(*CHECK_NONFATAL(chainman.ActiveTip()));
        },
    };
}

static RPCHelpMan getblockfrompeer() {
    return RPCHelpMan{
        "getblockfrompeer",
        "Attempt to fetch block from a given peer.\n"
        "\nWe must have the header for this block, e.g. using submitheader.\n"
        "The block will not have any undo data which can limit the usage of "
        "the block data in a context where the undo data is needed.\n"
        "Subsequent calls for the same block may cause the response from the "
        "previous peer to be ignored.\n"
        "\nReturns an empty JSON object if the request was successfully "
        "scheduled.",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "The block hash to try to fetch"},
            {"peer_id", RPCArg::Type::NUM, RPCArg::Optional::NO,
             "The peer to fetch it from (see getpeerinfo for peer IDs)"},
        },
        RPCResult{RPCResult::Type::OBJ, "", /*optional=*/false, "", {}},
        RPCExamples{HelpExampleCli("getblockfrompeer",
                                   "\"00000000c937983704a73af28acdec37b049d214a"
                                   "dbda81d7e2a3dd146f6ed09\" 0") +
                    HelpExampleRpc("getblockfrompeer",
                                   "\"00000000c937983704a73af28acdec37b049d214a"
                                   "dbda81d7e2a3dd146f6ed09\" 0")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            const NodeContext &node = EnsureAnyNodeContext(request.context);
            ChainstateManager &chainman = EnsureChainman(node);
            PeerManager &peerman = EnsurePeerman(node);

            const BlockHash block_hash{
                ParseHashV(request.params[0], "blockhash")};
            const NodeId peer_id{request.params[1].getInt<int64_t>()};

            const CBlockIndex *const index = WITH_LOCK(
                cs_main,
                return chainman.m_blockman.LookupBlockIndex(block_hash););

            if (!index) {
                throw JSONRPCError(RPC_MISC_ERROR, "Block header missing");
            }

            if (WITH_LOCK(::cs_main, return index->nStatus.hasData())) {
                throw JSONRPCError(RPC_MISC_ERROR, "Block already downloaded");
            }

            if (const auto err{peerman.FetchBlock(config, peer_id, *index)}) {
                throw JSONRPCError(RPC_MISC_ERROR, err.value());
            }
            return UniValue::VOBJ;
        },
    };
}

static RPCHelpMan getblockhash() {
    return RPCHelpMan{
        "getblockhash",
        "Returns hash of block in best-block-chain at height provided.\n",
        {
            {"height", RPCArg::Type::NUM, RPCArg::Optional::NO,
             "The height index"},
        },
        RPCResult{RPCResult::Type::STR_HEX, "", "The block hash"},
        RPCExamples{HelpExampleCli("getblockhash", "1000") +
                    HelpExampleRpc("getblockhash", "1000")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            LOCK(cs_main);
            const CChain &active_chain = chainman.ActiveChain();

            int nHeight = request.params[0].getInt<int>();
            if (nHeight < 0 || nHeight > active_chain.Height()) {
                throw JSONRPCError(RPC_INVALID_PARAMETER,
                                   "Block height out of range");
            }

            const CBlockIndex *pblockindex = active_chain[nHeight];
            return pblockindex->GetBlockHash().GetHex();
        },
    };
}

static RPCHelpMan getblockheader() {
    return RPCHelpMan{
        "getblockheader",
        "If verbose is false, returns a string that is serialized, hex-encoded "
        "data for blockheader 'hash'.\n"
        "If verbose is true, returns an Object with information about "
        "blockheader <hash>.\n",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "The block hash"},
            {"verbose", RPCArg::Type::BOOL, RPCArg::Default{true},
             "true for a json object, false for the hex-encoded data"},
        },
        {
            RPCResult{
                "for verbose = true",
                RPCResult::Type::OBJ,
                "",
                "",
                {
                    {RPCResult::Type::STR_HEX, "hash",
                     "the block hash (same as provided)"},
                    {RPCResult::Type::NUM, "confirmations",
                     "The number of confirmations, or -1 if the block is not "
                     "on the main chain"},
                    {RPCResult::Type::NUM, "height",
                     "The block height or index"},
                    {RPCResult::Type::NUM, "version", "The block version"},
                    {RPCResult::Type::STR_HEX, "versionHex",
                     "The block version formatted in hexadecimal"},
                    {RPCResult::Type::STR_HEX, "merkleroot", "The merkle root"},
                    {RPCResult::Type::NUM_TIME, "time",
                     "The block time expressed in " + UNIX_EPOCH_TIME},
                    {RPCResult::Type::NUM_TIME, "mediantime",
                     "The median block time expressed in " + UNIX_EPOCH_TIME},
                    {RPCResult::Type::NUM, "nonce", "The nonce"},
                    {RPCResult::Type::STR_HEX, "bits", "The bits"},
                    {RPCResult::Type::NUM, "difficulty", "The difficulty"},
                    {RPCResult::Type::STR_HEX, "chainwork",
                     "Expected number of hashes required to produce the "
                     "current chain"},
                    {RPCResult::Type::NUM, "nTx",
                     "The number of transactions in the block"},
                    {RPCResult::Type::STR_HEX, "previousblockhash",
                     /* optional */ true,
                     "The hash of the previous block (if available)"},
                    {RPCResult::Type::STR_HEX, "nextblockhash",
                     /* optional */ true,
                     "The hash of the next block (if available)"},
                }},
            RPCResult{"for verbose=false", RPCResult::Type::STR_HEX, "",
                      "A string that is serialized, hex-encoded data for block "
                      "'hash'"},
        },
        RPCExamples{HelpExampleCli("getblockheader",
                                   "\"00000000c937983704a73af28acdec37b049d214a"
                                   "dbda81d7e2a3dd146f6ed09\"") +
                    HelpExampleRpc("getblockheader",
                                   "\"00000000c937983704a73af28acdec37b049d214a"
                                   "dbda81d7e2a3dd146f6ed09\"")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            BlockHash hash(ParseHashV(request.params[0], "hash"));

            bool fVerbose = true;
            if (!request.params[1].isNull()) {
                fVerbose = request.params[1].get_bool();
            }

            const CBlockIndex *pblockindex;
            const CBlockIndex *tip;
            {
                ChainstateManager &chainman =
                    EnsureAnyChainman(request.context);
                LOCK(cs_main);
                pblockindex = chainman.m_blockman.LookupBlockIndex(hash);
                tip = chainman.ActiveTip();
            }

            if (!pblockindex) {
                throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
                                   "Block not found");
            }

            if (!fVerbose) {
                DataStream ssBlock{};
                ssBlock << pblockindex->GetBlockHeader();
                std::string strHex = HexStr(ssBlock);
                return strHex;
            }

            return blockheaderToJSON(*tip, *pblockindex);
        },
    };
}

static CBlock GetBlockChecked(BlockManager &blockman,
                              const CBlockIndex &blockindex) {
    CBlock block;
    {
        LOCK(cs_main);
        if (blockman.IsBlockPruned(blockindex)) {
            throw JSONRPCError(RPC_MISC_ERROR,
                               "Block not available (pruned data)");
        }
    }

    if (!blockman.ReadBlockFromDisk(block, blockindex)) {
        // Block not found on disk. This could be because we have the block
        // header in our index but not yet have the block or did not accept the
        // block. Or if the block was pruned right after we released the lock
        // above.
        throw JSONRPCError(RPC_MISC_ERROR, "Block not found on disk");
    }

    return block;
}

static CBlockUndo GetUndoChecked(BlockManager &blockman,
                                 const CBlockIndex &blockindex) {
    CBlockUndo blockUndo;

    {
        LOCK(cs_main);
        if (blockman.IsBlockPruned(blockindex)) {
            throw JSONRPCError(RPC_MISC_ERROR,
                               "Undo data not available (pruned data)");
        }
    }

    if (!blockman.UndoReadFromDisk(blockUndo, blockindex)) {
        throw JSONRPCError(RPC_MISC_ERROR, "Can't read undo data from disk");
    }

    return blockUndo;
}

static RPCHelpMan getblock() {
    return RPCHelpMan{
        "getblock",
        "If verbosity is 0 or false, returns a string that is serialized, "
        "hex-encoded data for block 'hash'.\n"
        "If verbosity is 1 or true, returns an Object with information about "
        "block <hash>.\n"
        "If verbosity is 2, returns an Object with information about block "
        "<hash> and information about each transaction.\n"
        "If verbosity is 3, returns an Object with information about block "
        "<hash> and information about each transaction, including prevout "
        "information for inputs (only for unpruned blocks in the current best "
        "chain).\n",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "The block hash"},
            {"verbosity|verbose", RPCArg::Type::NUM, RPCArg::Default{1},
             "0 for hex-encoded data, 1 for a json object, and 2 for json "
             "object with transaction data",
             RPCArgOptions{.skip_type_check = true}},
        },
        {
            RPCResult{"for verbosity = 0", RPCResult::Type::STR_HEX, "",
                      "A string that is serialized, hex-encoded data for block "
                      "'hash'"},
            RPCResult{
                "for verbosity = 1",
                RPCResult::Type::OBJ,
                "",
                "",
                {
                    {RPCResult::Type::STR_HEX, "hash",
                     "the block hash (same as provided)"},
                    {RPCResult::Type::NUM, "confirmations",
                     "The number of confirmations, or -1 if the block is not "
                     "on the main chain"},
                    {RPCResult::Type::NUM, "size", "The block size"},
                    {RPCResult::Type::NUM, "height",
                     "The block height or index"},
                    {RPCResult::Type::NUM, "version", "The block version"},
                    {RPCResult::Type::STR_HEX, "versionHex",
                     "The block version formatted in hexadecimal"},
                    {RPCResult::Type::STR_HEX, "merkleroot", "The merkle root"},
                    {RPCResult::Type::ARR,
                     "tx",
                     "The transaction ids",
                     {{RPCResult::Type::STR_HEX, "", "The transaction id"}}},
                    {RPCResult::Type::NUM_TIME, "time",
                     "The block time expressed in " + UNIX_EPOCH_TIME},
                    {RPCResult::Type::NUM_TIME, "mediantime",
                     "The median block time expressed in " + UNIX_EPOCH_TIME},
                    {RPCResult::Type::NUM, "nonce", "The nonce"},
                    {RPCResult::Type::STR_HEX, "bits", "The bits"},
                    {RPCResult::Type::NUM, "difficulty", "The difficulty"},
                    {RPCResult::Type::STR_HEX, "chainwork",
                     "Expected number of hashes required to produce the chain "
                     "up to this block (in hex)"},
                    {RPCResult::Type::NUM, "nTx",
                     "The number of transactions in the block"},
                    {RPCResult::Type::STR_HEX, "previousblockhash",
                     /* optional */ true,
                     "The hash of the previous block (if available)"},
                    {RPCResult::Type::STR_HEX, "nextblockhash",
                     /* optional */ true,
                     "The hash of the next block (if available)"},
                }},
            RPCResult{"for verbosity = 2",
                      RPCResult::Type::OBJ,
                      "",
                      "",
                      {
                          {RPCResult::Type::ELISION, "",
                           "Same output as verbosity = 1"},
                          {RPCResult::Type::ARR,
                           "tx",
                           "",
                           {
                               {RPCResult::Type::OBJ,
                                "",
                                "",
                                {
                                    {RPCResult::Type::ELISION, "",
                                     "The transactions in the format of the "
                                     "getrawtransaction RPC. Different from "
                                     "verbosity = 1 \"tx\" result"},
                                    {RPCResult::Type::STR_AMOUNT, "fee",
                                     "The transaction fee in " +
                                         Currency::get().ticker +
                                         ", omitted if block undo data is not "
                                         "available"},
                                }},
                           }},
                      }},
        },
        RPCExamples{
            HelpExampleCli("getblock", "\"00000000c937983704a73af28acdec37b049d"
                                       "214adbda81d7e2a3dd146f6ed09\"") +
            HelpExampleRpc("getblock", "\"00000000c937983704a73af28acdec37b049d"
                                       "214adbda81d7e2a3dd146f6ed09\"")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            BlockHash hash(ParseHashV(request.params[0], "blockhash"));

            int verbosity = 1;
            if (!request.params[1].isNull()) {
                if (request.params[1].isNum()) {
                    verbosity = request.params[1].getInt<int>();
                } else {
                    verbosity = request.params[1].get_bool() ? 1 : 0;
                }
            }

            const CBlockIndex *pblockindex;
            const CBlockIndex *tip;
            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            {
                LOCK(cs_main);
                pblockindex = chainman.m_blockman.LookupBlockIndex(hash);
                tip = chainman.ActiveTip();

                if (!pblockindex) {
                    throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
                                       "Block not found");
                }
            }

            const CBlock block =
                GetBlockChecked(chainman.m_blockman, *pblockindex);

            if (verbosity <= 0) {
                CDataStream ssBlock(SER_NETWORK, PROTOCOL_VERSION);
                ssBlock << block;
                std::string strHex = HexStr(ssBlock);
                return strHex;
            }

            TxVerbosity tx_verbosity;
            if (verbosity == 1) {
                tx_verbosity = TxVerbosity::SHOW_TXID;
            } else if (verbosity == 2) {
                tx_verbosity = TxVerbosity::SHOW_DETAILS;
            } else {
                tx_verbosity = TxVerbosity::SHOW_DETAILS_AND_PREVOUT;
            }

            return blockToJSON(chainman.m_blockman, block, *tip, *pblockindex,
                               tx_verbosity);
        },
    };
}

std::optional<int> GetPruneHeight(const BlockManager &blockman,
                                  const CChain &chain) {
    AssertLockHeld(::cs_main);

    // Search for the last block missing block data or undo data. Don't let the
    // search consider the genesis block, because the genesis block does not
    // have undo data, but should not be considered pruned.
    const CBlockIndex *first_block{chain[1]};
    const CBlockIndex *chain_tip{chain.Tip()};

    // If there are no blocks after the genesis block, or no blocks at all,
    // nothing is pruned.
    if (!first_block || !chain_tip) {
        return std::nullopt;
    }

    // If the chain tip is pruned, everything is pruned.
    if (!(chain_tip->nStatus.hasData() && chain_tip->nStatus.hasUndo())) {
        return chain_tip->nHeight;
    }

    // Get first block with data, after the last block without data.
    // This is the start of the unpruned range of blocks.
    const CBlockIndex *first_unpruned{CHECK_NONFATAL(
        blockman.GetFirstBlock(*chain_tip,
                               /*status_test=*/[](const BlockStatus &status) {
                                   return status.hasData() && status.hasUndo();
                               }))};
    if (first_unpruned == first_block) {
        // All blocks between first_block and chain_tip have data, so nothing is
        // pruned.
        return std::nullopt;
    }

    // Block before the first unpruned block is the last pruned block.
    return CHECK_NONFATAL(first_unpruned->pprev)->nHeight;
}

static RPCHelpMan pruneblockchain() {
    return RPCHelpMan{
        "pruneblockchain",
        "",
        {
            {"height", RPCArg::Type::NUM, RPCArg::Optional::NO,
             "The block height to prune up to. May be set to a discrete "
             "height, or to a " +
                 UNIX_EPOCH_TIME +
                 "\n"
                 "                  to prune blocks whose block time is at "
                 "least 2 hours older than the provided timestamp."},
        },
        RPCResult{RPCResult::Type::NUM, "", "Height of the last block pruned"},
        RPCExamples{HelpExampleCli("pruneblockchain", "1000") +
                    HelpExampleRpc("pruneblockchain", "1000")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            if (!chainman.m_blockman.IsPruneMode()) {
                throw JSONRPCError(
                    RPC_MISC_ERROR,
                    "Cannot prune blocks because node is not in prune mode.");
            }

            LOCK(cs_main);
            Chainstate &active_chainstate = chainman.ActiveChainstate();
            CChain &active_chain = active_chainstate.m_chain;

            int heightParam = request.params[0].getInt<int>();
            if (heightParam < 0) {
                throw JSONRPCError(RPC_INVALID_PARAMETER,
                                   "Negative block height.");
            }

            // Height value more than a billion is too high to be a block
            // height, and too low to be a block time (corresponds to timestamp
            // from Sep 2001).
            if (heightParam > 1000000000) {
                // Add a 2 hour buffer to include blocks which might have had
                // old timestamps
                const CBlockIndex *pindex = active_chain.FindEarliestAtLeast(
                    heightParam - TIMESTAMP_WINDOW, 0);
                if (!pindex) {
                    throw JSONRPCError(RPC_INVALID_PARAMETER,
                                       "Could not find block with at least the "
                                       "specified timestamp.");
                }
                heightParam = pindex->nHeight;
            }

            unsigned int height = (unsigned int)heightParam;
            unsigned int chainHeight = (unsigned int)active_chain.Height();
            if (chainHeight < config.GetChainParams().PruneAfterHeight()) {
                throw JSONRPCError(RPC_MISC_ERROR,
                                   "Blockchain is too short for pruning.");
            } else if (height > chainHeight) {
                throw JSONRPCError(
                    RPC_INVALID_PARAMETER,
                    "Blockchain is shorter than the attempted prune height.");
            } else if (height > chainHeight - MIN_BLOCKS_TO_KEEP) {
                LogPrint(BCLog::RPC,
                         "Attempt to prune blocks close to the tip. "
                         "Retaining the minimum number of blocks.\n");
                height = chainHeight - MIN_BLOCKS_TO_KEEP;
            }

            PruneBlockFilesManual(active_chainstate, height);
            return GetPruneHeight(chainman.m_blockman, active_chain)
                .value_or(-1);
        },
    };
}

static CoinStatsHashType ParseHashType(const std::string &hash_type_input) {
    if (hash_type_input == "hash_serialized") {
        return CoinStatsHashType::HASH_SERIALIZED;
    } else if (hash_type_input == "muhash") {
        return CoinStatsHashType::MUHASH;
    } else if (hash_type_input == "none") {
        return CoinStatsHashType::NONE;
    } else {
        throw JSONRPCError(
            RPC_INVALID_PARAMETER,
            strprintf("%s is not a valid hash_type", hash_type_input));
    }
}

static RPCHelpMan gettxoutsetinfo() {
    return RPCHelpMan{
        "gettxoutsetinfo",
        "Returns statistics about the unspent transaction output set.\n"
        "Note this call may take some time if you are not using "
        "coinstatsindex.\n",
        {
            {"hash_type", RPCArg::Type::STR, RPCArg::Default{"hash_serialized"},
             "Which UTXO set hash should be calculated. Options: "
             "'hash_serialized' (the legacy algorithm), 'muhash', 'none'."},
            {"hash_or_height", RPCArg::Type::NUM, RPCArg::Optional::OMITTED,
             "The block hash or height of the target height (only available "
             "with coinstatsindex).",
             RPCArgOptions{.skip_type_check = true,
                           .type_str = {"", "string or numeric"}}},
            {"use_index", RPCArg::Type::BOOL, RPCArg::Default{true},
             "Use coinstatsindex, if available."},
        },
        RPCResult{
            RPCResult::Type::OBJ,
            "",
            "",
            {
                {RPCResult::Type::NUM, "height",
                 "The current block height (index)"},
                {RPCResult::Type::STR_HEX, "bestblock",
                 "The hash of the block at the tip of the chain"},
                {RPCResult::Type::NUM, "txouts",
                 "The number of unspent transaction outputs"},
                {RPCResult::Type::NUM, "bogosize",
                 "Database-independent, meaningless metric indicating "
                 "the UTXO set size"},
                {RPCResult::Type::STR_HEX, "hash_serialized",
                 /* optional */ true,
                 "The serialized hash (only present if 'hash_serialized' "
                 "hash_type is chosen)"},
                {RPCResult::Type::STR_HEX, "muhash", /* optional */ true,
                 "The serialized hash (only present if 'muhash' "
                 "hash_type is chosen)"},
                {RPCResult::Type::NUM, "transactions",
                 "The number of transactions with unspent outputs (not "
                 "available when coinstatsindex is used)"},
                {RPCResult::Type::NUM, "disk_size",
                 "The estimated size of the chainstate on disk (not "
                 "available when coinstatsindex is used)"},
                {RPCResult::Type::STR_AMOUNT, "total_amount",
                 "The total amount"},
                {RPCResult::Type::STR_AMOUNT, "total_unspendable_amount",
                 "The total amount of coins permanently excluded from the UTXO "
                 "set (only available if coinstatsindex is used)"},
                {RPCResult::Type::OBJ,
                 "block_info",
                 "Info on amounts in the block at this block height (only "
                 "available if coinstatsindex is used)",
                 {{RPCResult::Type::STR_AMOUNT, "prevout_spent",
                   "Total amount of all prevouts spent in this block"},
                  {RPCResult::Type::STR_AMOUNT, "coinbase",
                   "Coinbase subsidy amount of this block"},
                  {RPCResult::Type::STR_AMOUNT, "new_outputs_ex_coinbase",
                   "Total amount of new outputs created by this block"},
                  {RPCResult::Type::STR_AMOUNT, "unspendable",
                   "Total amount of unspendable outputs created in this block"},
                  {RPCResult::Type::OBJ,
                   "unspendables",
                   "Detailed view of the unspendable categories",
                   {
                       {RPCResult::Type::STR_AMOUNT, "genesis_block",
                        "The unspendable amount of the Genesis block subsidy"},
                       {RPCResult::Type::STR_AMOUNT, "bip30",
                        "Transactions overridden by duplicates (no longer "
                        "possible with BIP30)"},
                       {RPCResult::Type::STR_AMOUNT, "scripts",
                        "Amounts sent to scripts that are unspendable (for "
                        "example OP_RETURN outputs)"},
                       {RPCResult::Type::STR_AMOUNT, "unclaimed_rewards",
                        "Fee rewards that miners did not claim in their "
                        "coinbase transaction"},
                   }}}},
            }},
        RPCExamples{
            HelpExampleCli("gettxoutsetinfo", "") +
            HelpExampleCli("gettxoutsetinfo", R"("none")") +
            HelpExampleCli("gettxoutsetinfo", R"("none" 1000)") +
            HelpExampleCli(
                "gettxoutsetinfo",
                R"("none" '"00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09"')") +
            HelpExampleRpc("gettxoutsetinfo", "") +
            HelpExampleRpc("gettxoutsetinfo", R"("none")") +
            HelpExampleRpc("gettxoutsetinfo", R"("none", 1000)") +
            HelpExampleRpc(
                "gettxoutsetinfo",
                R"("none", "00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09")")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            UniValue ret(UniValue::VOBJ);

            const CBlockIndex *pindex{nullptr};
            const CoinStatsHashType hash_type{
                request.params[0].isNull()
                    ? CoinStatsHashType::HASH_SERIALIZED
                    : ParseHashType(request.params[0].get_str())};
            bool index_requested =
                request.params[2].isNull() || request.params[2].get_bool();

            NodeContext &node = EnsureAnyNodeContext(request.context);
            ChainstateManager &chainman = EnsureChainman(node);
            Chainstate &active_chainstate = chainman.ActiveChainstate();
            active_chainstate.ForceFlushStateToDisk();

            CCoinsView *coins_view;
            BlockManager *blockman;
            {
                LOCK(::cs_main);
                coins_view = &active_chainstate.CoinsDB();
                blockman = &active_chainstate.m_blockman;
                pindex = blockman->LookupBlockIndex(coins_view->GetBestBlock());
            }

            if (!request.params[1].isNull()) {
                if (!g_coin_stats_index) {
                    throw JSONRPCError(RPC_INVALID_PARAMETER,
                                       "Querying specific block heights "
                                       "requires coinstatsindex");
                }

                if (hash_type == CoinStatsHashType::HASH_SERIALIZED) {
                    throw JSONRPCError(RPC_INVALID_PARAMETER,
                                       "hash_serialized hash type cannot be "
                                       "queried for a specific block");
                }

                pindex = ParseHashOrHeight(request.params[1], chainman);
            }

            if (index_requested && g_coin_stats_index) {
                if (!g_coin_stats_index->BlockUntilSyncedToCurrentChain()) {
                    const IndexSummary summary{
                        g_coin_stats_index->GetSummary()};

                    // If a specific block was requested and the index has
                    // already synced past that height, we can return the data
                    // already even though the index is not fully synced yet.
                    if (pindex->nHeight > summary.best_block_height) {
                        throw JSONRPCError(
                            RPC_INTERNAL_ERROR,
                            strprintf(
                                "Unable to get data because coinstatsindex is "
                                "still syncing. Current height: %d",
                                summary.best_block_height));
                    }
                }
            }

            const std::optional<CCoinsStats> maybe_stats = GetUTXOStats(
                coins_view, *blockman, hash_type, node.rpc_interruption_point,
                pindex, index_requested);
            if (maybe_stats.has_value()) {
                const CCoinsStats &stats = maybe_stats.value();
                ret.pushKV("height", int64_t(stats.nHeight));
                ret.pushKV("bestblock", stats.hashBlock.GetHex());
                ret.pushKV("txouts", int64_t(stats.nTransactionOutputs));
                ret.pushKV("bogosize", int64_t(stats.nBogoSize));
                if (hash_type == CoinStatsHashType::HASH_SERIALIZED) {
                    ret.pushKV("hash_serialized",
                               stats.hashSerialized.GetHex());
                }
                if (hash_type == CoinStatsHashType::MUHASH) {
                    ret.pushKV("muhash", stats.hashSerialized.GetHex());
                }
                ret.pushKV("total_amount", stats.nTotalAmount);
                if (!stats.index_used) {
                    ret.pushKV("transactions",
                               static_cast<int64_t>(stats.nTransactions));
                    ret.pushKV("disk_size", stats.nDiskSize);
                } else {
                    ret.pushKV("total_unspendable_amount",
                               stats.total_unspendable_amount);

                    CCoinsStats prev_stats{};
                    if (pindex->nHeight > 0) {
                        const std::optional<CCoinsStats> maybe_prev_stats =
                            GetUTXOStats(coins_view, *blockman, hash_type,
                                         node.rpc_interruption_point,
                                         pindex->pprev, index_requested);
                        if (!maybe_prev_stats) {
                            throw JSONRPCError(RPC_INTERNAL_ERROR,
                                               "Unable to read UTXO set");
                        }
                        prev_stats = maybe_prev_stats.value();
                    }

                    UniValue block_info(UniValue::VOBJ);
                    block_info.pushKV(
                        "prevout_spent",
                        stats.total_prevout_spent_amount -
                            prev_stats.total_prevout_spent_amount);
                    block_info.pushKV("coinbase",
                                      stats.total_coinbase_amount -
                                          prev_stats.total_coinbase_amount);
                    block_info.pushKV(
                        "new_outputs_ex_coinbase",
                        stats.total_new_outputs_ex_coinbase_amount -
                            prev_stats.total_new_outputs_ex_coinbase_amount);
                    block_info.pushKV("unspendable",
                                      stats.total_unspendable_amount -
                                          prev_stats.total_unspendable_amount);

                    UniValue unspendables(UniValue::VOBJ);
                    unspendables.pushKV(
                        "genesis_block",
                        stats.total_unspendables_genesis_block -
                            prev_stats.total_unspendables_genesis_block);
                    unspendables.pushKV(
                        "bip30", stats.total_unspendables_bip30 -
                                     prev_stats.total_unspendables_bip30);
                    unspendables.pushKV(
                        "scripts", stats.total_unspendables_scripts -
                                       prev_stats.total_unspendables_scripts);
                    unspendables.pushKV(
                        "unclaimed_rewards",
                        stats.total_unspendables_unclaimed_rewards -
                            prev_stats.total_unspendables_unclaimed_rewards);
                    block_info.pushKV("unspendables", unspendables);

                    ret.pushKV("block_info", block_info);
                }
            } else {
                throw JSONRPCError(RPC_INTERNAL_ERROR,
                                   "Unable to read UTXO set");
            }
            return ret;
        },
    };
}

RPCHelpMan gettxout() {
    return RPCHelpMan{
        "gettxout",
        "Returns details about an unspent transaction output.\n",
        {
            {"txid", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "The transaction id"},
            {"n", RPCArg::Type::NUM, RPCArg::Optional::NO, "vout number"},
            {"include_mempool", RPCArg::Type::BOOL, RPCArg::Default{true},
             "Whether to include the mempool. Note that an unspent output that "
             "is spent in the mempool won't appear."},
        },
        {
            RPCResult{"If the UTXO was not found", RPCResult::Type::NONE, "",
                      ""},
            RPCResult{
                "Otherwise",
                RPCResult::Type::OBJ,
                "",
                "",
                {
                    {RPCResult::Type::STR_HEX, "bestblock",
                     "The hash of the block at the tip of the chain"},
                    {RPCResult::Type::NUM, "confirmations",
                     "The number of confirmations"},
                    {RPCResult::Type::STR_AMOUNT, "value",
                     "The transaction value in " + Currency::get().ticker},
                    {RPCResult::Type::OBJ,
                     "scriptPubKey",
                     "",
                     {
                         {RPCResult::Type::STR_HEX, "asm", ""},
                         {RPCResult::Type::STR_HEX, "hex", ""},
                         {RPCResult::Type::NUM, "reqSigs",
                          "Number of required signatures"},
                         {RPCResult::Type::STR_HEX, "type",
                          "The type, eg pubkeyhash"},
                         {RPCResult::Type::ARR,
                          "addresses",
                          "array of eCash addresses",
                          {{RPCResult::Type::STR, "address", "eCash address"}}},
                     }},
                    {RPCResult::Type::BOOL, "coinbase", "Coinbase or not"},
                }},
        },
        RPCExamples{"\nGet unspent transactions\n" +
                    HelpExampleCli("listunspent", "") + "\nView the details\n" +
                    HelpExampleCli("gettxout", "\"txid\" 1") +
                    "\nAs a JSON-RPC call\n" +
                    HelpExampleRpc("gettxout", "\"txid\", 1")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            NodeContext &node = EnsureAnyNodeContext(request.context);
            ChainstateManager &chainman = EnsureChainman(node);
            LOCK(cs_main);

            UniValue ret(UniValue::VOBJ);

            TxId txid(ParseHashV(request.params[0], "txid"));
            int n = request.params[1].getInt<int>();
            COutPoint out(txid, n);
            bool fMempool = true;
            if (!request.params[2].isNull()) {
                fMempool = request.params[2].get_bool();
            }

            Coin coin;
            Chainstate &active_chainstate = chainman.ActiveChainstate();
            CCoinsViewCache *coins_view = &active_chainstate.CoinsTip();

            if (fMempool) {
                const CTxMemPool &mempool = EnsureMemPool(node);
                LOCK(mempool.cs);
                CCoinsViewMemPool view(coins_view, mempool);
                if (!view.GetCoin(out, coin) || mempool.isSpent(out)) {
                    return NullUniValue;
                }
            } else {
                if (!coins_view->GetCoin(out, coin)) {
                    return NullUniValue;
                }
            }

            const CBlockIndex *pindex =
                active_chainstate.m_blockman.LookupBlockIndex(
                    coins_view->GetBestBlock());
            ret.pushKV("bestblock", pindex->GetBlockHash().GetHex());
            if (coin.GetHeight() == MEMPOOL_HEIGHT) {
                ret.pushKV("confirmations", 0);
            } else {
                ret.pushKV("confirmations",
                           int64_t(pindex->nHeight - coin.GetHeight() + 1));
            }
            ret.pushKV("value", coin.GetTxOut().nValue);
            UniValue o(UniValue::VOBJ);
            ScriptPubKeyToUniv(coin.GetTxOut().scriptPubKey, o, true);
            ret.pushKV("scriptPubKey", o);
            ret.pushKV("coinbase", coin.IsCoinBase());

            return ret;
        },
    };
}

static RPCHelpMan verifychain() {
    return RPCHelpMan{
        "verifychain",
        "Verifies blockchain database.\n",
        {
            {"checklevel", RPCArg::Type::NUM,
             RPCArg::DefaultHint{
                 strprintf("%d, range=0-4", DEFAULT_CHECKLEVEL)},
             strprintf("How thorough the block verification is:\n%s",
                       MakeUnorderedList(CHECKLEVEL_DOC))},
            {"nblocks", RPCArg::Type::NUM,
             RPCArg::DefaultHint{strprintf("%d, 0=all", DEFAULT_CHECKBLOCKS)},
             "The number of blocks to check."},
        },
        RPCResult{RPCResult::Type::BOOL, "",
                  "Verification finished successfully. If false, check "
                  "debug.log for reason."},
        RPCExamples{HelpExampleCli("verifychain", "") +
                    HelpExampleRpc("verifychain", "")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            const int check_level{request.params[0].isNull()
                                      ? DEFAULT_CHECKLEVEL
                                      : request.params[0].getInt<int>()};
            const int check_depth{request.params[1].isNull()
                                      ? DEFAULT_CHECKBLOCKS
                                      : request.params[1].getInt<int>()};

            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            LOCK(cs_main);

            Chainstate &active_chainstate = chainman.ActiveChainstate();
            return CVerifyDB(chainman.GetNotifications())
                       .VerifyDB(active_chainstate,
                                 active_chainstate.CoinsTip(), check_level,
                                 check_depth) == VerifyDBResult::SUCCESS;
        },
    };
}

RPCHelpMan getblockchaininfo() {
    return RPCHelpMan{
        "getblockchaininfo",
        "Returns an object containing various state info regarding blockchain "
        "processing.\n",
        {},
        RPCResult{
            RPCResult::Type::OBJ,
            "",
            "",
            {
                {RPCResult::Type::STR, "chain",
                 "current network name (main, test, regtest)"},
                {RPCResult::Type::NUM, "blocks",
                 "the height of the most-work fully-validated "
                 "non-parked chain. The genesis block has height 0"},
                {RPCResult::Type::NUM, "headers",
                 "the current number of headers we have validated"},
                {RPCResult::Type::NUM, "finalized_blockhash",
                 "the hash of the avalanche finalized tip if any, otherwise "
                 "the genesis block hash"},
                {RPCResult::Type::STR, "bestblockhash",
                 "the hash of the currently best block"},
                {RPCResult::Type::NUM, "difficulty", "the current difficulty"},
                {RPCResult::Type::NUM_TIME, "time",
                 "The block time expressed in " + UNIX_EPOCH_TIME},
                {RPCResult::Type::NUM_TIME, "mediantime",
                 "The median block time expressed in " + UNIX_EPOCH_TIME},
                {RPCResult::Type::NUM, "verificationprogress",
                 "estimate of verification progress [0..1]"},
                {RPCResult::Type::BOOL, "initialblockdownload",
                 "(debug information) estimate of whether this node is in "
                 "Initial Block Download mode"},
                {RPCResult::Type::STR_HEX, "chainwork",
                 "total amount of work in active chain, in hexadecimal"},
                {RPCResult::Type::NUM, "size_on_disk",
                 "the estimated size of the block and undo files on disk"},
                {RPCResult::Type::BOOL, "pruned",
                 "if the blocks are subject to pruning"},
                {RPCResult::Type::NUM, "pruneheight",
                 "lowest-height complete block stored (only present if pruning "
                 "is enabled)"},
                {RPCResult::Type::BOOL, "automatic_pruning",
                 "whether automatic pruning is enabled (only present if "
                 "pruning is enabled)"},
                {RPCResult::Type::NUM, "prune_target_size",
                 "the target size used by pruning (only present if automatic "
                 "pruning is enabled)"},
                {RPCResult::Type::STR, "warnings",
                 "any network and blockchain warnings"},
            }},
        RPCExamples{HelpExampleCli("getblockchaininfo", "") +
                    HelpExampleRpc("getblockchaininfo", "")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            const CChainParams &chainparams = config.GetChainParams();

            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            LOCK(cs_main);
            Chainstate &active_chainstate = chainman.ActiveChainstate();

            const CBlockIndex &tip{
                *CHECK_NONFATAL(active_chainstate.m_chain.Tip())};
            const int height{tip.nHeight};

            UniValue obj(UniValue::VOBJ);
            obj.pushKV("chain", chainparams.GetChainTypeString());
            obj.pushKV("blocks", height);
            obj.pushKV("headers", chainman.m_best_header
                                      ? chainman.m_best_header->nHeight
                                      : -1);
            auto avalanche_finalized_tip{chainman.GetAvalancheFinalizedTip()};
            obj.pushKV("finalized_blockhash",
                       avalanche_finalized_tip
                           ? avalanche_finalized_tip->GetBlockHash().GetHex()
                           : chainparams.GenesisBlock().GetHash().GetHex());
            obj.pushKV("bestblockhash", tip.GetBlockHash().GetHex());
            obj.pushKV("difficulty", GetDifficulty(tip));
            obj.pushKV("time", tip.GetBlockTime());
            obj.pushKV("mediantime", tip.GetMedianTimePast());
            obj.pushKV(
                "verificationprogress",
                GuessVerificationProgress(chainman.GetParams().TxData(), &tip));
            obj.pushKV("initialblockdownload",
                       chainman.IsInitialBlockDownload());
            obj.pushKV("chainwork", tip.nChainWork.GetHex());
            obj.pushKV("size_on_disk",
                       chainman.m_blockman.CalculateCurrentUsage());
            obj.pushKV("pruned", chainman.m_blockman.IsPruneMode());

            if (chainman.m_blockman.IsPruneMode()) {
                const auto prune_height{GetPruneHeight(
                    chainman.m_blockman, active_chainstate.m_chain)};
                obj.pushKV("pruneheight",
                           prune_height ? prune_height.value() + 1 : 0);

                const bool automatic_pruning{
                    chainman.m_blockman.GetPruneTarget() !=
                    BlockManager::PRUNE_TARGET_MANUAL};
                obj.pushKV("automatic_pruning", automatic_pruning);
                if (automatic_pruning) {
                    obj.pushKV("prune_target_size",
                               chainman.m_blockman.GetPruneTarget());
                }
            }

            obj.pushKV("warnings", GetWarnings(false).original);
            return obj;
        },
    };
}

/** Comparison function for sorting the getchaintips heads.  */
struct CompareBlocksByHeight {
    bool operator()(const CBlockIndex *a, const CBlockIndex *b) const {
        // Make sure that unequal blocks with the same height do not compare
        // equal. Use the pointers themselves to make a distinction.
        if (a->nHeight != b->nHeight) {
            return (a->nHeight > b->nHeight);
        }

        return a < b;
    }
};

static RPCHelpMan getchaintips() {
    return RPCHelpMan{
        "getchaintips",
        "Return information about all known tips in the block tree, including "
        "the main chain as well as orphaned branches.\n",
        {},
        RPCResult{
            RPCResult::Type::ARR,
            "",
            "",
            {{RPCResult::Type::OBJ,
              "",
              "",
              {
                  {RPCResult::Type::NUM, "height", "height of the chain tip"},
                  {RPCResult::Type::STR_HEX, "hash", "block hash of the tip"},
                  {RPCResult::Type::NUM, "branchlen",
                   "zero for main chain, otherwise length of branch connecting "
                   "the tip to the main chain"},
                  {RPCResult::Type::STR, "status",
                   "status of the chain, \"active\" for the main chain\n"
                   "Possible values for status:\n"
                   "1.  \"invalid\"               This branch contains at "
                   "least one invalid block\n"
                   "2.  \"parked\"                This branch contains at "
                   "least one parked block\n"
                   "3.  \"headers-only\"          Not all blocks for this "
                   "branch are available, but the headers are valid\n"
                   "4.  \"valid-headers\"         All blocks are available for "
                   "this branch, but they were never fully validated\n"
                   "5.  \"valid-fork\"            This branch is not part of "
                   "the active chain, but is fully validated\n"
                   "6.  \"active\"                This is the tip of the "
                   "active main chain, which is certainly valid"},
              }}}},
        RPCExamples{HelpExampleCli("getchaintips", "") +
                    HelpExampleRpc("getchaintips", "")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            LOCK(cs_main);
            CChain &active_chain = chainman.ActiveChain();

            /**
             * Idea: The set of chain tips is the active chain tip, plus orphan
             * blocks which do not have another orphan building off of them.
             * Algorithm:
             *  - Make one pass through BlockIndex(), picking out the orphan
             * blocks, and also storing a set of the orphan block's pprev
             * pointers.
             *  - Iterate through the orphan blocks. If the block isn't pointed
             * to by another orphan, it is a chain tip.
             *  - Add the active chain tip
             */
            std::set<const CBlockIndex *, CompareBlocksByHeight> setTips;
            std::set<const CBlockIndex *> setOrphans;
            std::set<const CBlockIndex *> setPrevs;

            for (const auto &[_, block_index] : chainman.BlockIndex()) {
                if (!active_chain.Contains(&block_index)) {
                    setOrphans.insert(&block_index);
                    setPrevs.insert(block_index.pprev);
                }
            }

            for (std::set<const CBlockIndex *>::iterator it =
                     setOrphans.begin();
                 it != setOrphans.end(); ++it) {
                if (setPrevs.erase(*it) == 0) {
                    setTips.insert(*it);
                }
            }

            // Always report the currently active tip.
            setTips.insert(active_chain.Tip());

            /* Construct the output array.  */
            UniValue res(UniValue::VARR);
            for (const CBlockIndex *block : setTips) {
                UniValue obj(UniValue::VOBJ);
                obj.pushKV("height", block->nHeight);
                obj.pushKV("hash", block->phashBlock->GetHex());

                const int branchLen =
                    block->nHeight - active_chain.FindFork(block)->nHeight;
                obj.pushKV("branchlen", branchLen);

                std::string status;
                if (active_chain.Contains(block)) {
                    // This block is part of the currently active chain.
                    status = "active";
                } else if (block->nStatus.isInvalid()) {
                    // This block or one of its ancestors is invalid.
                    status = "invalid";
                } else if (block->nStatus.isOnParkedChain()) {
                    // This block or one of its ancestors is parked.
                    status = "parked";
                } else if (!block->HaveNumChainTxs()) {
                    // This block cannot be connected because full block data
                    // for it or one of its parents is missing.
                    status = "headers-only";
                } else if (block->IsValid(BlockValidity::SCRIPTS)) {
                    // This block is fully validated, but no longer part of the
                    // active chain. It was probably the active block once, but
                    // was reorganized.
                    status = "valid-fork";
                } else if (block->IsValid(BlockValidity::TREE)) {
                    // The headers for this block are valid, but it has not been
                    // validated. It was probably never part of the most-work
                    // chain.
                    status = "valid-headers";
                } else {
                    // No clue.
                    status = "unknown";
                }
                obj.pushKV("status", status);

                res.push_back(obj);
            }

            return res;
        },
    };
}

static RPCHelpMan preciousblock() {
    return RPCHelpMan{
        "preciousblock",
        "Treats a block as if it were received before others with the same "
        "work.\n"
        "\nA later preciousblock call can override the effect of an earlier "
        "one.\n"
        "\nThe effects of preciousblock are not retained across restarts.\n",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "the hash of the block to mark as precious"},
        },
        RPCResult{RPCResult::Type::NONE, "", ""},
        RPCExamples{HelpExampleCli("preciousblock", "\"blockhash\"") +
                    HelpExampleRpc("preciousblock", "\"blockhash\"")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            BlockHash hash(ParseHashV(request.params[0], "blockhash"));
            CBlockIndex *pblockindex;

            NodeContext &node = EnsureAnyNodeContext(request.context);
            ChainstateManager &chainman = EnsureChainman(node);
            {
                LOCK(cs_main);
                pblockindex = chainman.m_blockman.LookupBlockIndex(hash);
                if (!pblockindex) {
                    throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
                                       "Block not found");
                }
            }

            BlockValidationState state;
            chainman.ActiveChainstate().PreciousBlock(state, pblockindex,
                                                      node.avalanche.get());

            if (!state.IsValid()) {
                throw JSONRPCError(RPC_DATABASE_ERROR, state.GetRejectReason());
            }

            // Block to make sure wallet/indexers sync before returning
            SyncWithValidationInterfaceQueue();

            return NullUniValue;
        },
    };
}

static void InvalidateBlock(ChainstateManager &chainman,
                            avalanche::Processor *const avalanche,
                            const BlockHash &block_hash) {
    BlockValidationState state;
    CBlockIndex *pblockindex;
    {
        LOCK(chainman.GetMutex());
        pblockindex = chainman.m_blockman.LookupBlockIndex(block_hash);
        if (!pblockindex) {
            throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
        }
    }
    chainman.ActiveChainstate().InvalidateBlock(state, pblockindex);

    if (state.IsValid()) {
        chainman.ActiveChainstate().ActivateBestChain(state, /*pblock=*/nullptr,
                                                      avalanche);
    }

    if (!state.IsValid()) {
        throw JSONRPCError(RPC_DATABASE_ERROR, state.ToString());
    }
}

static RPCHelpMan invalidateblock() {
    return RPCHelpMan{
        "invalidateblock",
        "Permanently marks a block as invalid, as if it violated a consensus "
        "rule.\n",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "the hash of the block to mark as invalid"},
        },
        RPCResult{RPCResult::Type::NONE, "", ""},
        RPCExamples{HelpExampleCli("invalidateblock", "\"blockhash\"") +
                    HelpExampleRpc("invalidateblock", "\"blockhash\"")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            NodeContext &node = EnsureAnyNodeContext(request.context);
            ChainstateManager &chainman = EnsureChainman(node);
            const BlockHash hash(ParseHashV(request.params[0], "blockhash"));

            InvalidateBlock(chainman, node.avalanche.get(), hash);
            // Block to make sure wallet/indexers sync before returning
            SyncWithValidationInterfaceQueue();

            return NullUniValue;
        },
    };
}

RPCHelpMan parkblock() {
    return RPCHelpMan{
        "parkblock",
        "Marks a block as parked.\n",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "the hash of the block to park"},
        },
        RPCResult{RPCResult::Type::NONE, "", ""},
        RPCExamples{HelpExampleCli("parkblock", "\"blockhash\"") +
                    HelpExampleRpc("parkblock", "\"blockhash\"")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            const std::string strHash = request.params[0].get_str();
            const BlockHash hash(uint256S(strHash));
            BlockValidationState state;

            NodeContext &node = EnsureAnyNodeContext(request.context);
            ChainstateManager &chainman = EnsureChainman(node);
            Chainstate &active_chainstate = chainman.ActiveChainstate();
            CBlockIndex *pblockindex = nullptr;
            {
                LOCK(cs_main);
                pblockindex = chainman.m_blockman.LookupBlockIndex(hash);
                if (!pblockindex) {
                    throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
                                       "Block not found");
                }

                if (active_chainstate.IsBlockAvalancheFinalized(pblockindex)) {
                    // Reset avalanche finalization if we park a finalized
                    // block.
                    active_chainstate.ClearAvalancheFinalizedBlock();
                }
            }

            active_chainstate.ParkBlock(state, pblockindex);

            if (state.IsValid()) {
                active_chainstate.ActivateBestChain(state, /*pblock=*/nullptr,
                                                    node.avalanche.get());
            }

            if (!state.IsValid()) {
                throw JSONRPCError(RPC_DATABASE_ERROR, state.GetRejectReason());
            }

            // Block to make sure wallet/indexers sync before returning
            SyncWithValidationInterfaceQueue();

            return NullUniValue;
        },
    };
}

static void ReconsiderBlock(ChainstateManager &chainman,
                            avalanche::Processor *const avalanche,
                            const BlockHash &block_hash) {
    {
        LOCK(chainman.GetMutex());
        CBlockIndex *pblockindex =
            chainman.m_blockman.LookupBlockIndex(block_hash);
        if (!pblockindex) {
            throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
        }

        chainman.ActiveChainstate().ResetBlockFailureFlags(pblockindex);
        chainman.RecalculateBestHeader();
    }

    BlockValidationState state;
    chainman.ActiveChainstate().ActivateBestChain(state, /*pblock=*/nullptr,
                                                  avalanche);

    if (!state.IsValid()) {
        throw JSONRPCError(RPC_DATABASE_ERROR, state.ToString());
    }
}

static RPCHelpMan reconsiderblock() {
    return RPCHelpMan{
        "reconsiderblock",
        "Removes invalidity status of a block, its ancestors and its"
        "descendants, reconsider them for activation.\n"
        "This can be used to undo the effects of invalidateblock.\n",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "the hash of the block to reconsider"},
        },
        RPCResult{RPCResult::Type::NONE, "", ""},
        RPCExamples{HelpExampleCli("reconsiderblock", "\"blockhash\"") +
                    HelpExampleRpc("reconsiderblock", "\"blockhash\"")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            NodeContext &node = EnsureAnyNodeContext(request.context);
            ChainstateManager &chainman = EnsureChainman(node);
            const BlockHash hash(ParseHashV(request.params[0], "blockhash"));

            ReconsiderBlock(chainman, node.avalanche.get(), hash);

            // Block to make sure wallet/indexers sync before returning
            SyncWithValidationInterfaceQueue();

            return NullUniValue;
        },
    };
}

RPCHelpMan unparkblock() {
    return RPCHelpMan{
        "unparkblock",
        "Removes parked status of a block and its descendants, reconsider "
        "them for activation.\n"
        "This can be used to undo the effects of parkblock.\n",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "the hash of the block to unpark"},
        },
        RPCResult{RPCResult::Type::NONE, "", ""},
        RPCExamples{HelpExampleCli("unparkblock", "\"blockhash\"") +
                    HelpExampleRpc("unparkblock", "\"blockhash\"")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            const std::string strHash = request.params[0].get_str();
            NodeContext &node = EnsureAnyNodeContext(request.context);
            ChainstateManager &chainman = EnsureChainman(node);
            const BlockHash hash(uint256S(strHash));
            Chainstate &active_chainstate = chainman.ActiveChainstate();

            {
                LOCK(cs_main);

                CBlockIndex *pblockindex =
                    chainman.m_blockman.LookupBlockIndex(hash);
                if (!pblockindex) {
                    throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
                                       "Block not found");
                }

                if (!pblockindex->nStatus.isOnParkedChain()) {
                    // Block to unpark is not parked so there is nothing to do.
                    return NullUniValue;
                }

                const CBlockIndex *tip = active_chainstate.m_chain.Tip();
                if (tip) {
                    const CBlockIndex *ancestor =
                        LastCommonAncestor(tip, pblockindex);
                    if (active_chainstate.IsBlockAvalancheFinalized(ancestor)) {
                        // Only reset avalanche finalization if we unpark a
                        // block that might conflict with avalanche finalized
                        // blocks.
                        active_chainstate.ClearAvalancheFinalizedBlock();
                    }
                }

                active_chainstate.UnparkBlockAndChildren(pblockindex);
            }

            BlockValidationState state;
            active_chainstate.ActivateBestChain(state, /*pblock=*/nullptr,
                                                node.avalanche.get());

            if (!state.IsValid()) {
                throw JSONRPCError(RPC_DATABASE_ERROR, state.GetRejectReason());
            }

            // Block to make sure wallet/indexers sync before returning
            SyncWithValidationInterfaceQueue();

            return NullUniValue;
        },
    };
}

static RPCHelpMan getchaintxstats() {
    return RPCHelpMan{
        "getchaintxstats",
        "Compute statistics about the total number and rate of transactions "
        "in the chain.\n",
        {
            {"nblocks", RPCArg::Type::NUM, RPCArg::DefaultHint{"one month"},
             "Size of the window in number of blocks"},
            {"blockhash", RPCArg::Type::STR_HEX,
             RPCArg::DefaultHint{"chain tip"},
             "The hash of the block that ends the window."},
        },
        RPCResult{
            RPCResult::Type::OBJ,
            "",
            "",
            {
                {RPCResult::Type::NUM_TIME, "time",
                 "The timestamp for the final block in the window, "
                 "expressed in " +
                     UNIX_EPOCH_TIME},
                {RPCResult::Type::NUM, "txcount", /*optional=*/true,
                 "The total number of transactions in the chain up to "
                 "that point, if known. It may be unknown when using "
                 "assumeutxo."},
                {RPCResult::Type::STR_HEX, "window_final_block_hash",
                 "The hash of the final block in the window"},
                {RPCResult::Type::NUM, "window_final_block_height",
                 "The height of the final block in the window."},
                {RPCResult::Type::NUM, "window_block_count",
                 "Size of the window in number of blocks"},
                {RPCResult::Type::NUM, "window_interval",
                 "The elapsed time in the window in seconds. Only "
                 "returned if \"window_block_count\" is > 0"},
                {RPCResult::Type::NUM, "window_tx_count", /*optional=*/true,
                 "The number of transactions in the window. Only "
                 "returned if \"window_block_count\" is > 0 and if "
                 "txcount exists for the start and end of the window."},
                {RPCResult::Type::NUM, "txrate", /*optional=*/true,
                 "The average rate of transactions per second in the "
                 "window. Only returned if \"window_interval\" is > 0 "
                 "and if window_tx_count exists."},
            }},
        RPCExamples{HelpExampleCli("getchaintxstats", "") +
                    HelpExampleRpc("getchaintxstats", "2016")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            const CBlockIndex *pindex;

            // By default: 1 month
            int blockcount =
                30 * 24 * 60 * 60 /
                config.GetChainParams().GetConsensus().nPowTargetSpacing;

            if (request.params[1].isNull()) {
                LOCK(cs_main);
                pindex = chainman.ActiveTip();
            } else {
                BlockHash hash(ParseHashV(request.params[1], "blockhash"));
                LOCK(cs_main);
                pindex = chainman.m_blockman.LookupBlockIndex(hash);
                if (!pindex) {
                    throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
                                       "Block not found");
                }
                if (!chainman.ActiveChain().Contains(pindex)) {
                    throw JSONRPCError(RPC_INVALID_PARAMETER,
                                       "Block is not in main chain");
                }
            }

            CHECK_NONFATAL(pindex != nullptr);

            if (request.params[0].isNull()) {
                blockcount =
                    std::max(0, std::min(blockcount, pindex->nHeight - 1));
            } else {
                blockcount = request.params[0].getInt<int>();

                if (blockcount < 0 ||
                    (blockcount > 0 && blockcount >= pindex->nHeight)) {
                    throw JSONRPCError(RPC_INVALID_PARAMETER,
                                       "Invalid block count: "
                                       "should be between 0 and "
                                       "the block's height - 1");
                }
            }

            const CBlockIndex &past_block{*CHECK_NONFATAL(
                pindex->GetAncestor(pindex->nHeight - blockcount))};
            const int64_t nTimeDiff{pindex->GetMedianTimePast() -
                                    past_block.GetMedianTimePast()};

            UniValue ret(UniValue::VOBJ);
            ret.pushKV("time", pindex->GetBlockTime());
            if (pindex->nChainTx) {
                ret.pushKV("txcount", pindex->nChainTx);
            }
            ret.pushKV("window_final_block_hash",
                       pindex->GetBlockHash().GetHex());
            ret.pushKV("window_final_block_height", pindex->nHeight);
            ret.pushKV("window_block_count", blockcount);
            if (blockcount > 0) {
                ret.pushKV("window_interval", nTimeDiff);
                if (pindex->nChainTx != 0 && past_block.nChainTx != 0) {
                    unsigned int window_tx_count =
                        pindex->nChainTx - past_block.nChainTx;
                    ret.pushKV("window_tx_count", window_tx_count);
                    if (nTimeDiff > 0) {
                        ret.pushKV("txrate",
                                   double(window_tx_count) / nTimeDiff);
                    }
                }
            }

            return ret;
        },
    };
}

template <typename T>
static T CalculateTruncatedMedian(std::vector<T> &scores) {
    size_t size = scores.size();
    if (size == 0) {
        return T();
    }

    std::sort(scores.begin(), scores.end());
    if (size % 2 == 0) {
        return (scores[size / 2 - 1] + scores[size / 2]) / 2;
    } else {
        return scores[size / 2];
    }
}

template <typename T> static inline bool SetHasKeys(const std::set<T> &set) {
    return false;
}
template <typename T, typename Tk, typename... Args>
static inline bool SetHasKeys(const std::set<T> &set, const Tk &key,
                              const Args &...args) {
    return (set.count(key) != 0) || SetHasKeys(set, args...);
}

// outpoint (needed for the utxo index) + nHeight + fCoinBase
static constexpr size_t PER_UTXO_OVERHEAD =
    sizeof(COutPoint) + sizeof(uint32_t) + sizeof(bool);

static RPCHelpMan getblockstats() {
    const auto &ticker = Currency::get().ticker;
    return RPCHelpMan{
        "getblockstats",
        "Compute per block statistics for a given window. All amounts are "
        "in " +
            ticker +
            ".\n"
            "It won't work for some heights with pruning.\n",
        {
            {"hash_or_height", RPCArg::Type::NUM, RPCArg::Optional::NO,
             "The block hash or height of the target block",
             RPCArgOptions{.skip_type_check = true,
                           .type_str = {"", "string or numeric"}}},
            {"stats",
             RPCArg::Type::ARR,
             RPCArg::DefaultHint{"all values"},
             "Values to plot (see result below)",
             {
                 {"height", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
                  "Selected statistic"},
                 {"time", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
                  "Selected statistic"},
             },
             RPCArgOptions{.oneline_description = "stats"}},
        },
        RPCResult{
            RPCResult::Type::OBJ,
            "",
            "",
            {
                {RPCResult::Type::NUM, "avgfee", "Average fee in the block"},
                {RPCResult::Type::NUM, "avgfeerate",
                 "Average feerate (in satoshis per virtual byte)"},
                {RPCResult::Type::NUM, "avgtxsize", "Average transaction size"},
                {RPCResult::Type::STR_HEX, "blockhash",
                 "The block hash (to check for potential reorgs)"},
                {RPCResult::Type::NUM, "height", "The height of the block"},
                {RPCResult::Type::NUM, "ins",
                 "The number of inputs (excluding coinbase)"},
                {RPCResult::Type::NUM, "maxfee", "Maximum fee in the block"},
                {RPCResult::Type::NUM, "maxfeerate",
                 "Maximum feerate (in satoshis per virtual byte)"},
                {RPCResult::Type::NUM, "maxtxsize", "Maximum transaction size"},
                {RPCResult::Type::NUM, "medianfee",
                 "Truncated median fee in the block"},
                {RPCResult::Type::NUM, "medianfeerate",
                 "Truncated median feerate (in " + ticker + " per byte)"},
                {RPCResult::Type::NUM, "mediantime",
                 "The block median time past"},
                {RPCResult::Type::NUM, "mediantxsize",
                 "Truncated median transaction size"},
                {RPCResult::Type::NUM, "minfee", "Minimum fee in the block"},
                {RPCResult::Type::NUM, "minfeerate",
                 "Minimum feerate (in satoshis per virtual byte)"},
                {RPCResult::Type::NUM, "mintxsize", "Minimum transaction size"},
                {RPCResult::Type::NUM, "outs", "The number of outputs"},
                {RPCResult::Type::NUM, "subsidy", "The block subsidy"},
                {RPCResult::Type::NUM, "time", "The block time"},
                {RPCResult::Type::NUM, "total_out",
                 "Total amount in all outputs (excluding coinbase and thus "
                 "reward [ie subsidy + totalfee])"},
                {RPCResult::Type::NUM, "total_size",
                 "Total size of all non-coinbase transactions"},
                {RPCResult::Type::NUM, "totalfee", "The fee total"},
                {RPCResult::Type::NUM, "txs",
                 "The number of transactions (including coinbase)"},
                {RPCResult::Type::NUM, "utxo_increase",
                 "The increase/decrease in the number of unspent outputs"},
                {RPCResult::Type::NUM, "utxo_size_inc",
                 "The increase/decrease in size for the utxo index (not "
                 "discounting op_return and similar)"},
            }},
        RPCExamples{
            HelpExampleCli(
                "getblockstats",
                R"('"00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09"' '["minfeerate","avgfeerate"]')") +
            HelpExampleCli("getblockstats",
                           R"(1000 '["minfeerate","avgfeerate"]')") +
            HelpExampleRpc(
                "getblockstats",
                R"("00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09", ["minfeerate","avgfeerate"])") +
            HelpExampleRpc("getblockstats",
                           R"(1000, ["minfeerate","avgfeerate"])")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            ChainstateManager &chainman = EnsureAnyChainman(request.context);
            const CBlockIndex &pindex{*CHECK_NONFATAL(
                ParseHashOrHeight(request.params[0], chainman))};

            std::set<std::string> stats;
            if (!request.params[1].isNull()) {
                const UniValue stats_univalue = request.params[1].get_array();
                for (unsigned int i = 0; i < stats_univalue.size(); i++) {
                    const std::string stat = stats_univalue[i].get_str();
                    stats.insert(stat);
                }
            }

            const CBlock &block = GetBlockChecked(chainman.m_blockman, pindex);
            const CBlockUndo &blockUndo =
                GetUndoChecked(chainman.m_blockman, pindex);

            // Calculate everything if nothing selected (default)
            const bool do_all = stats.size() == 0;
            const bool do_mediantxsize =
                do_all || stats.count("mediantxsize") != 0;
            const bool do_medianfee = do_all || stats.count("medianfee") != 0;
            const bool do_medianfeerate =
                do_all || stats.count("medianfeerate") != 0;
            const bool loop_inputs =
                do_all || do_medianfee || do_medianfeerate ||
                SetHasKeys(stats, "utxo_size_inc", "totalfee", "avgfee",
                           "avgfeerate", "minfee", "maxfee", "minfeerate",
                           "maxfeerate");
            const bool loop_outputs =
                do_all || loop_inputs || stats.count("total_out");
            const bool do_calculate_size =
                do_mediantxsize || loop_inputs ||
                SetHasKeys(stats, "total_size", "avgtxsize", "mintxsize",
                           "maxtxsize");

            const int64_t blockMaxSize = config.GetMaxBlockSize();
            Amount maxfee = Amount::zero();
            Amount maxfeerate = Amount::zero();
            Amount minfee = MAX_MONEY;
            Amount minfeerate = MAX_MONEY;
            Amount total_out = Amount::zero();
            Amount totalfee = Amount::zero();
            int64_t inputs = 0;
            int64_t maxtxsize = 0;
            int64_t mintxsize = blockMaxSize;
            int64_t outputs = 0;
            int64_t total_size = 0;
            int64_t utxo_size_inc = 0;
            std::vector<Amount> fee_array;
            std::vector<Amount> feerate_array;
            std::vector<int64_t> txsize_array;

            for (size_t i = 0; i < block.vtx.size(); ++i) {
                const auto &tx = block.vtx.at(i);
                outputs += tx->vout.size();
                Amount tx_total_out = Amount::zero();
                if (loop_outputs) {
                    for (const CTxOut &out : tx->vout) {
                        tx_total_out += out.nValue;
                        utxo_size_inc +=
                            GetSerializeSize(out, PROTOCOL_VERSION) +
                            PER_UTXO_OVERHEAD;
                    }
                }

                if (tx->IsCoinBase()) {
                    continue;
                }

                // Don't count coinbase's fake input
                inputs += tx->vin.size();
                // Don't count coinbase reward
                total_out += tx_total_out;

                int64_t tx_size = 0;
                if (do_calculate_size) {
                    tx_size = tx->GetTotalSize();
                    if (do_mediantxsize) {
                        txsize_array.push_back(tx_size);
                    }
                    maxtxsize = std::max(maxtxsize, tx_size);
                    mintxsize = std::min(mintxsize, tx_size);
                    total_size += tx_size;
                }

                if (loop_inputs) {
                    Amount tx_total_in = Amount::zero();
                    const auto &txundo = blockUndo.vtxundo.at(i - 1);
                    for (const Coin &coin : txundo.vprevout) {
                        const CTxOut &prevoutput = coin.GetTxOut();

                        tx_total_in += prevoutput.nValue;
                        utxo_size_inc -=
                            GetSerializeSize(prevoutput, PROTOCOL_VERSION) +
                            PER_UTXO_OVERHEAD;
                    }

                    Amount txfee = tx_total_in - tx_total_out;
                    CHECK_NONFATAL(MoneyRange(txfee));
                    if (do_medianfee) {
                        fee_array.push_back(txfee);
                    }
                    maxfee = std::max(maxfee, txfee);
                    minfee = std::min(minfee, txfee);
                    totalfee += txfee;

                    Amount feerate = txfee / tx_size;
                    if (do_medianfeerate) {
                        feerate_array.push_back(feerate);
                    }
                    maxfeerate = std::max(maxfeerate, feerate);
                    minfeerate = std::min(minfeerate, feerate);
                }
            }

            UniValue ret_all(UniValue::VOBJ);
            ret_all.pushKV("avgfee",
                           block.vtx.size() > 1
                               ? (totalfee / int((block.vtx.size() - 1)))
                               : Amount::zero());
            ret_all.pushKV("avgfeerate", total_size > 0
                                             ? (totalfee / total_size)
                                             : Amount::zero());
            ret_all.pushKV("avgtxsize",
                           (block.vtx.size() > 1)
                               ? total_size / (block.vtx.size() - 1)
                               : 0);
            ret_all.pushKV("blockhash", pindex.GetBlockHash().GetHex());
            ret_all.pushKV("height", (int64_t)pindex.nHeight);
            ret_all.pushKV("ins", inputs);
            ret_all.pushKV("maxfee", maxfee);
            ret_all.pushKV("maxfeerate", maxfeerate);
            ret_all.pushKV("maxtxsize", maxtxsize);
            ret_all.pushKV("medianfee", CalculateTruncatedMedian(fee_array));
            ret_all.pushKV("medianfeerate",
                           CalculateTruncatedMedian(feerate_array));
            ret_all.pushKV("mediantime", pindex.GetMedianTimePast());
            ret_all.pushKV("mediantxsize",
                           CalculateTruncatedMedian(txsize_array));
            ret_all.pushKV("minfee",
                           minfee == MAX_MONEY ? Amount::zero() : minfee);
            ret_all.pushKV("minfeerate", minfeerate == MAX_MONEY
                                             ? Amount::zero()
                                             : minfeerate);
            ret_all.pushKV("mintxsize",
                           mintxsize == blockMaxSize ? 0 : mintxsize);
            ret_all.pushKV("outs", outputs);
            ret_all.pushKV("subsidy", GetBlockSubsidy(pindex.nHeight,
                                                      chainman.GetConsensus()));
            ret_all.pushKV("time", pindex.GetBlockTime());
            ret_all.pushKV("total_out", total_out);
            ret_all.pushKV("total_size", total_size);
            ret_all.pushKV("totalfee", totalfee);
            ret_all.pushKV("txs", (int64_t)block.vtx.size());
            ret_all.pushKV("utxo_increase", outputs - inputs);
            ret_all.pushKV("utxo_size_inc", utxo_size_inc);

            if (do_all) {
                return ret_all;
            }

            UniValue ret(UniValue::VOBJ);
            for (const std::string &stat : stats) {
                const UniValue &value = ret_all[stat];
                if (value.isNull()) {
                    throw JSONRPCError(
                        RPC_INVALID_PARAMETER,
                        strprintf("Invalid selected statistic %s", stat));
                }
                ret.pushKV(stat, value);
            }
            return ret;
        },
    };
}

namespace {
//! Search for a given set of pubkey scripts
static bool FindScriptPubKey(std::atomic<int> &scan_progress,
                             const std::atomic<bool> &should_abort,
                             int64_t &count, CCoinsViewCursor *cursor,
                             const std::set<CScript> &needles,
                             std::map<COutPoint, Coin> &out_results,
                             std::function<void()> &interruption_point) {
    scan_progress = 0;
    count = 0;
    while (cursor->Valid()) {
        COutPoint key;
        Coin coin;
        if (!cursor->GetKey(key) || !cursor->GetValue(coin)) {
            return false;
        }
        if (++count % 8192 == 0) {
            interruption_point();
            if (should_abort) {
                // allow to abort the scan via the abort reference
                return false;
            }
        }
        if (count % 256 == 0) {
            // update progress reference every 256 item
            const TxId &txid = key.GetTxId();
            uint32_t high = 0x100 * *txid.begin() + *(txid.begin() + 1);
            scan_progress = int(high * 100.0 / 65536.0 + 0.5);
        }
        if (needles.count(coin.GetTxOut().scriptPubKey)) {
            out_results.emplace(key, coin);
        }
        cursor->Next();
    }
    scan_progress = 100;
    return true;
}
} // namespace

/** RAII object to prevent concurrency issue when scanning the txout set */
static std::atomic<int> g_scan_progress;
static std::atomic<bool> g_scan_in_progress;
static std::atomic<bool> g_should_abort_scan;
class CoinsViewScanReserver {
private:
    bool m_could_reserve{false};

public:
    explicit CoinsViewScanReserver() = default;

    bool reserve() {
        CHECK_NONFATAL(!m_could_reserve);
        if (g_scan_in_progress.exchange(true)) {
            return false;
        }
        m_could_reserve = true;
        return true;
    }

    ~CoinsViewScanReserver() {
        if (m_could_reserve) {
            g_scan_in_progress = false;
        }
    }
};

static RPCHelpMan scantxoutset() {
    const auto &ticker = Currency::get().ticker;
    return RPCHelpMan{
        "scantxoutset",
        "Scans the unspent transaction output set for entries that match "
        "certain output descriptors.\n"
        "Examples of output descriptors are:\n"
        "    addr(<address>)                      Outputs whose scriptPubKey "
        "corresponds to the specified address (does not include P2PK)\n"
        "    raw(<hex script>)                    Outputs whose scriptPubKey "
        "equals the specified hex scripts\n"
        "    combo(<pubkey>)                      P2PK and P2PKH outputs for "
        "the given pubkey\n"
        "    pkh(<pubkey>)                        P2PKH outputs for the given "
        "pubkey\n"
        "    sh(multi(<n>,<pubkey>,<pubkey>,...)) P2SH-multisig outputs for "
        "the given threshold and pubkeys\n"
        "\nIn the above, <pubkey> either refers to a fixed public key in "
        "hexadecimal notation, or to an xpub/xprv optionally followed by one\n"
        "or more path elements separated by \"/\", and optionally ending in "
        "\"/*\" (unhardened), or \"/*'\" or \"/*h\" (hardened) to specify all\n"
        "unhardened or hardened child keys.\n"
        "In the latter case, a range needs to be specified by below if "
        "different from 1000.\n"
        "For more information on output descriptors, see the documentation in "
        "the doc/descriptors.md file.\n",
        {
            {"action", RPCArg::Type::STR, RPCArg::Optional::NO,
             "The action to execute\n"
             "                                      \"start\" for starting a "
             "scan\n"
             "                                      \"abort\" for aborting the "
             "current scan (returns true when abort was successful)\n"
             "                                      \"status\" for "
             "progress report (in %) of the current scan"},
            {"scanobjects",
             RPCArg::Type::ARR,
             RPCArg::Optional::OMITTED,
             "Array of scan objects. Required for \"start\" action\n"
             "                                  Every scan object is either a "
             "string descriptor or an object:",
             {
                 {"descriptor", RPCArg::Type::STR, RPCArg::Optional::OMITTED,
                  "An output descriptor"},
                 {
                     "",
                     RPCArg::Type::OBJ,
                     RPCArg::Optional::OMITTED,
                     "An object with output descriptor and metadata",
                     {
                         {"desc", RPCArg::Type::STR, RPCArg::Optional::NO,
                          "An output descriptor"},
                         {"range", RPCArg::Type::RANGE, RPCArg::Default{1000},
                          "The range of HD chain indexes to explore (either "
                          "end or [begin,end])"},
                     },
                 },
             },
             RPCArgOptions{.oneline_description = "[scanobjects,...]"}},
        },
        {
            RPCResult{"When action=='abort'", RPCResult::Type::BOOL, "", ""},
            RPCResult{"When action=='status' and no scan is in progress",
                      RPCResult::Type::NONE, "", ""},
            RPCResult{
                "When action=='status' and scan is in progress",
                RPCResult::Type::OBJ,
                "",
                "",
                {
                    {RPCResult::Type::NUM, "progress", "The scan progress"},
                }},
            RPCResult{
                "When action=='start'",
                RPCResult::Type::OBJ,
                "",
                "",
                {
                    {RPCResult::Type::BOOL, "success",
                     "Whether the scan was completed"},
                    {RPCResult::Type::NUM, "txouts",
                     "The number of unspent transaction outputs scanned"},
                    {RPCResult::Type::NUM, "height",
                     "The current block height (index)"},
                    {RPCResult::Type::STR_HEX, "bestblock",
                     "The hash of the block at the tip of the chain"},
                    {RPCResult::Type::ARR,
                     "unspents",
                     "",
                     {
                         {RPCResult::Type::OBJ,
                          "",
                          "",
                          {
                              {RPCResult::Type::STR_HEX, "txid",
                               "The transaction id"},
                              {RPCResult::Type::NUM, "vout", "The vout value"},
                              {RPCResult::Type::STR_HEX, "scriptPubKey",
                               "The script key"},
                              {RPCResult::Type::STR, "desc",
                               "A specialized descriptor for the matched "
                               "scriptPubKey"},
                              {RPCResult::Type::STR_AMOUNT, "amount",
                               "The total amount in " + ticker +
                                   " of the unspent output"},
                              {RPCResult::Type::BOOL, "coinbase",
                               "Whether this is a coinbase output"},
                              {RPCResult::Type::NUM, "height",
                               "Height of the unspent transaction output"},
                          }},
                     }},
                    {RPCResult::Type::STR_AMOUNT, "total_amount",
                     "The total amount of all found unspent outputs in " +
                         ticker},
                }},
        },
        RPCExamples{""},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            UniValue result(UniValue::VOBJ);
            const auto action{self.Arg<std::string>("action")};
            if (action == "status") {
                CoinsViewScanReserver reserver;
                if (reserver.reserve()) {
                    // no scan in progress
                    return NullUniValue;
                }
                result.pushKV("progress", g_scan_progress.load());
                return result;
            } else if (action == "abort") {
                CoinsViewScanReserver reserver;
                if (reserver.reserve()) {
                    // reserve was possible which means no scan was running
                    return false;
                }
                // set the abort flag
                g_should_abort_scan = true;
                return true;
            } else if (action == "start") {
                CoinsViewScanReserver reserver;
                if (!reserver.reserve()) {
                    throw JSONRPCError(RPC_INVALID_PARAMETER,
                                       "Scan already in progress, use action "
                                       "\"abort\" or \"status\"");
                }

                if (request.params.size() < 2) {
                    throw JSONRPCError(RPC_MISC_ERROR,
                                       "scanobjects argument is required for "
                                       "the start action");
                }

                std::set<CScript> needles;
                std::map<CScript, std::string> descriptors;
                Amount total_in = Amount::zero();

                // loop through the scan objects
                for (const UniValue &scanobject :
                     request.params[1].get_array().getValues()) {
                    FlatSigningProvider provider;
                    auto scripts =
                        EvalDescriptorStringOrObject(scanobject, provider);
                    for (CScript &script : scripts) {
                        std::string inferred =
                            InferDescriptor(script, provider)->ToString();
                        needles.emplace(script);
                        descriptors.emplace(std::move(script),
                                            std::move(inferred));
                    }
                }

                // Scan the unspent transaction output set for inputs
                UniValue unspents(UniValue::VARR);
                std::vector<CTxOut> input_txos;
                std::map<COutPoint, Coin> coins;
                g_should_abort_scan = false;
                g_scan_progress = 0;
                int64_t count = 0;
                std::unique_ptr<CCoinsViewCursor> pcursor;
                const CBlockIndex *tip;
                NodeContext &node = EnsureAnyNodeContext(request.context);
                {
                    ChainstateManager &chainman = EnsureChainman(node);
                    LOCK(cs_main);
                    Chainstate &active_chainstate = chainman.ActiveChainstate();
                    active_chainstate.ForceFlushStateToDisk();
                    pcursor = CHECK_NONFATAL(std::unique_ptr<CCoinsViewCursor>(
                        active_chainstate.CoinsDB().Cursor()));
                    tip = CHECK_NONFATAL(active_chainstate.m_chain.Tip());
                }
                bool res = FindScriptPubKey(
                    g_scan_progress, g_should_abort_scan, count, pcursor.get(),
                    needles, coins, node.rpc_interruption_point);
                result.pushKV("success", res);
                result.pushKV("txouts", count);
                result.pushKV("height", tip->nHeight);
                result.pushKV("bestblock", tip->GetBlockHash().GetHex());

                for (const auto &it : coins) {
                    const COutPoint &outpoint = it.first;
                    const Coin &coin = it.second;
                    const CTxOut &txo = coin.GetTxOut();
                    input_txos.push_back(txo);
                    total_in += txo.nValue;

                    UniValue unspent(UniValue::VOBJ);
                    unspent.pushKV("txid", outpoint.GetTxId().GetHex());
                    unspent.pushKV("vout", int32_t(outpoint.GetN()));
                    unspent.pushKV("scriptPubKey", HexStr(txo.scriptPubKey));
                    unspent.pushKV("desc", descriptors[txo.scriptPubKey]);
                    unspent.pushKV("amount", txo.nValue);
                    unspent.pushKV("coinbase", coin.IsCoinBase());
                    unspent.pushKV("height", int32_t(coin.GetHeight()));

                    unspents.push_back(unspent);
                }
                result.pushKV("unspents", unspents);
                result.pushKV("total_amount", total_in);
            } else {
                throw JSONRPCError(RPC_INVALID_PARAMETER,
                                   strprintf("Invalid action '%s'", action));
            }
            return result;
        },
    };
}

static RPCHelpMan getblockfilter() {
    return RPCHelpMan{
        "getblockfilter",
        "Retrieve a BIP 157 content filter for a particular block.\n",
        {
            {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO,
             "The hash of the block"},
            {"filtertype", RPCArg::Type::STR, RPCArg::Default{"basic"},
             "The type name of the filter"},
        },
        RPCResult{RPCResult::Type::OBJ,
                  "",
                  "",
                  {
                      {RPCResult::Type::STR_HEX, "filter",
                       "the hex-encoded filter data"},
                      {RPCResult::Type::STR_HEX, "header",
                       "the hex-encoded filter header"},
                  }},
        RPCExamples{
            HelpExampleCli("getblockfilter",
                           "\"00000000c937983704a73af28acdec37b049d214a"
                           "dbda81d7e2a3dd146f6ed09\" \"basic\"") +
            HelpExampleRpc("getblockfilter",
                           "\"00000000c937983704a73af28acdec37b049d214adbda81d7"
                           "e2a3dd146f6ed09\", \"basic\"")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            const BlockHash block_hash(
                ParseHashV(request.params[0], "blockhash"));
            std::string filtertype_name = "basic";
            if (!request.params[1].isNull()) {
                filtertype_name = request.params[1].get_str();
            }

            BlockFilterType filtertype;
            if (!BlockFilterTypeByName(filtertype_name, filtertype)) {
                throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
                                   "Unknown filtertype");
            }

            BlockFilterIndex *index = GetBlockFilterIndex(filtertype);
            if (!index) {
                throw JSONRPCError(RPC_MISC_ERROR,
                                   "Index is not enabled for filtertype " +
                                       filtertype_name);
            }

            const CBlockIndex *block_index;
            bool block_was_connected;
            {
                ChainstateManager &chainman =
                    EnsureAnyChainman(request.context);
                LOCK(cs_main);
                block_index = chainman.m_blockman.LookupBlockIndex(block_hash);
                if (!block_index) {
                    throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY,
                                       "Block not found");
                }
                block_was_connected =
                    block_index->IsValid(BlockValidity::SCRIPTS);
            }

            bool index_ready = index->BlockUntilSyncedToCurrentChain();

            BlockFilter filter;
            uint256 filter_header;
            if (!index->LookupFilter(block_index, filter) ||
                !index->LookupFilterHeader(block_index, filter_header)) {
                int err_code;
                std::string errmsg = "Filter not found.";

                if (!block_was_connected) {
                    err_code = RPC_INVALID_ADDRESS_OR_KEY;
                    errmsg += " Block was not connected to active chain.";
                } else if (!index_ready) {
                    err_code = RPC_MISC_ERROR;
                    errmsg += " Block filters are still in the process of "
                              "being indexed.";
                } else {
                    err_code = RPC_INTERNAL_ERROR;
                    errmsg += " This error is unexpected and indicates index "
                              "corruption.";
                }

                throw JSONRPCError(err_code, errmsg);
            }

            UniValue ret(UniValue::VOBJ);
            ret.pushKV("filter", HexStr(filter.GetEncodedFilter()));
            ret.pushKV("header", filter_header.GetHex());
            return ret;
        },
    };
}

/**
 * RAII class that disables the network in its constructor and enables it in its
 * destructor.
 */
class NetworkDisable {
    CConnman &m_connman;

public:
    NetworkDisable(CConnman &connman) : m_connman(connman) {
        m_connman.SetNetworkActive(false);
        if (m_connman.GetNetworkActive()) {
            throw JSONRPCError(RPC_MISC_ERROR,
                               "Network activity could not be suspended.");
        }
    };
    ~NetworkDisable() { m_connman.SetNetworkActive(true); };
};

/**
 * RAII class that temporarily rolls back the local chain in it's constructor
 * and rolls it forward again in it's destructor.
 */
class TemporaryRollback {
    ChainstateManager &m_chainman;
    avalanche::Processor *const m_avalanche;
    const CBlockIndex &m_invalidate_index;

public:
    TemporaryRollback(ChainstateManager &chainman,
                      avalanche::Processor *const avalanche,
                      const CBlockIndex &index)
        : m_chainman(chainman), m_avalanche(avalanche),
          m_invalidate_index(index) {
        InvalidateBlock(m_chainman, m_avalanche,
                        m_invalidate_index.GetBlockHash());
    };
    ~TemporaryRollback() {
        ReconsiderBlock(m_chainman, m_avalanche,
                        m_invalidate_index.GetBlockHash());
    };
};

/**
 * Serialize the UTXO set to a file for loading elsewhere.
 *
 * @see SnapshotMetadata
 */
static RPCHelpMan dumptxoutset() {
    return RPCHelpMan{
        "dumptxoutset",
        "Write the serialized UTXO set to a file. This can be used in "
        "loadtxoutset afterwards if this snapshot height is supported in the "
        "chainparams as well.\n\n"
        "Unless the the \"latest\" type is requested, the node will roll back "
        "to the requested height and network activity will be suspended during "
        "this process. "
        "Because of this it is discouraged to interact with the node in any "
        "other way during the execution of this call to avoid inconsistent "
        "results and race conditions, particularly RPCs that interact with "
        "blockstorage.\n\n"
        "This call may take several minutes. Make sure to use no RPC timeout "
        "(bitcoin-cli -rpcclienttimeout=0)",

        {
            {"path", RPCArg::Type::STR, RPCArg::Optional::NO,
             "path to the output file. If relative, will be prefixed by "
             "datadir."},
            {"type", RPCArg::Type::STR, RPCArg::Default(""),
             "The type of snapshot to create. Can be \"latest\" to create a "
             "snapshot of the current UTXO set or \"rollback\" to temporarily "
             "roll back the state of the node to a historical block before "
             "creating the snapshot of a historical UTXO set. This parameter "
             "can be omitted if a separate \"rollback\" named parameter is "
             "specified indicating the height or hash of a specific historical "
             "block. If \"rollback\" is specified and separate \"rollback\" "
             "named parameter is not specified, this will roll back to the "
             "latest valid snapshot block that can currently be loaded with "
             "loadtxoutset."},
            {
                "options",
                RPCArg::Type::OBJ_NAMED_PARAMS,
                RPCArg::Optional::OMITTED,
                "",
                {
                    {"rollback", RPCArg::Type::NUM, RPCArg::Optional::OMITTED,
                     "Height or hash of the block to roll back to before "
                     "creating the snapshot. Note: The further this number is "
                     "from the tip, the longer this process will take. "
                     "Consider setting a higher -rpcclienttimeout value in "
                     "this case.",
                     RPCArgOptions{.skip_type_check = true,
                                   .type_str = {"", "string or numeric"}}},
                },
            },
        },
        RPCResult{RPCResult::Type::OBJ,
                  "",
                  "",
                  {
                      {RPCResult::Type::NUM, "coins_written",
                       "the number of coins written in the snapshot"},
                      {RPCResult::Type::STR_HEX, "base_hash",
                       "the hash of the base of the snapshot"},
                      {RPCResult::Type::NUM, "base_height",
                       "the height of the base of the snapshot"},
                      {RPCResult::Type::STR, "path",
                       "the absolute path that the snapshot was written to"},
                      {RPCResult::Type::STR_HEX, "txoutset_hash",
                       "the hash of the UTXO set contents"},
                      {RPCResult::Type::NUM, "nchaintx",
                       "the number of transactions in the chain up to and "
                       "including the base block"},
                  }},
        RPCExamples{HelpExampleCli("-rpcclienttimeout=0 dumptxoutset",
                                   "utxo.dat latest") +
                    HelpExampleCli("-rpcclienttimeout=0 dumptxoutset",
                                   "utxo.dat rollback") +
                    HelpExampleCli("-rpcclienttimeout=0 -named dumptxoutset",
                                   R"(utxo.dat rollback=853456)")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            NodeContext &node = EnsureAnyNodeContext(request.context);
            const CBlockIndex *tip{WITH_LOCK(
                ::cs_main, return node.chainman->ActiveChain().Tip())};
            const CBlockIndex *target_index{nullptr};
            const std::string snapshot_type{self.Arg<std::string>("type")};
            const UniValue options{request.params[2].isNull()
                                       ? UniValue::VOBJ
                                       : request.params[2]};
            if (options.exists("rollback")) {
                if (!snapshot_type.empty() && snapshot_type != "rollback") {
                    throw JSONRPCError(
                        RPC_INVALID_PARAMETER,
                        strprintf("Invalid snapshot type \"%s\" specified with "
                                  "rollback option",
                                  snapshot_type));
                }
                target_index =
                    ParseHashOrHeight(options["rollback"], *node.chainman);
            } else if (snapshot_type == "rollback") {
                auto snapshot_heights =
                    node.chainman->GetParams().GetAvailableSnapshotHeights();
                CHECK_NONFATAL(snapshot_heights.size() > 0);
                auto max_height = std::max_element(snapshot_heights.begin(),
                                                   snapshot_heights.end());
                target_index = ParseHashOrHeight(*max_height, *node.chainman);
            } else if (snapshot_type == "latest") {
                target_index = tip;
            } else {
                throw JSONRPCError(
                    RPC_INVALID_PARAMETER,
                    strprintf("Invalid snapshot type \"%s\" specified. Please "
                              "specify \"rollback\" or \"latest\"",
                              snapshot_type));
            }

            const ArgsManager &args{EnsureAnyArgsman(request.context)};
            const fs::path path = fsbridge::AbsPathJoin(
                args.GetDataDirNet(), fs::u8path(request.params[0].get_str()));
            // Write to a temporary path and then move into `path` on completion
            // to avoid confusion due to an interruption.
            const fs::path temppath = fsbridge::AbsPathJoin(
                args.GetDataDirNet(),
                fs::u8path(request.params[0].get_str() + ".incomplete"));

            if (fs::exists(path)) {
                throw JSONRPCError(RPC_INVALID_PARAMETER,
                                   path.u8string() +
                                       " already exists. If you are sure this "
                                       "is what you want, "
                                       "move it out of the way first");
            }

            FILE *file{fsbridge::fopen(temppath, "wb")};
            AutoFile afile{file};

            CConnman &connman = EnsureConnman(node);
            const CBlockIndex *invalidate_index{nullptr};
            std::optional<NetworkDisable> disable_network;
            std::optional<TemporaryRollback> temporary_rollback;

            // If the user wants to dump the txoutset of the current tip, we
            // don't have to roll back at all
            if (target_index != tip) {
                // If the node is running in pruned mode we ensure all necessary
                // block data is available before starting to roll back.
                if (node.chainman->m_blockman.IsPruneMode()) {
                    LOCK(node.chainman->GetMutex());
                    const CBlockIndex *current_tip{
                        node.chainman->ActiveChain().Tip()};
                    const CBlockIndex *first_block{
                        node.chainman->m_blockman.GetFirstBlock(
                            *current_tip,
                            /*status_test=*/[](const BlockStatus &status) {
                                return status.hasData() && status.hasUndo();
                            })};
                    if (first_block->nHeight > target_index->nHeight) {
                        throw JSONRPCError(
                            RPC_MISC_ERROR,
                            "Could not roll back to requested height since "
                            "necessary block data is already pruned.");
                    }
                }

                // Suspend network activity for the duration of the process when
                // we are rolling back the chain to get a utxo set from a past
                // height. We do this so we don't punish peers that send us that
                // send us data that seems wrong in this temporary state. For
                // example a normal new block would be classified as a block
                // connecting an invalid block.
                // Skip if the network is already disabled because this
                // automatically re-enables the network activity at the end of
                // the process which may not be what the user wants.
                if (connman.GetNetworkActive()) {
                    disable_network.emplace(connman);
                }

                invalidate_index = WITH_LOCK(
                    ::cs_main,
                    return node.chainman->ActiveChain().Next(target_index));
                temporary_rollback.emplace(*node.chainman, node.avalanche.get(),
                                           *invalidate_index);
            }

            Chainstate *chainstate;
            std::unique_ptr<CCoinsViewCursor> cursor;
            CCoinsStats stats;
            {
                // Lock the chainstate before calling PrepareUtxoSnapshot, to
                // be able to get a UTXO database cursor while the chain is
                // pointing at the target block. After that, release the lock
                // while calling WriteUTXOSnapshot. The cursor will remain
                // valid and be used by WriteUTXOSnapshot to write a consistent
                // snapshot even if the chainstate changes.
                LOCK(node.chainman->GetMutex());
                chainstate = &node.chainman->ActiveChainstate();

                // In case there is any issue with a block being read from disk
                // we need to stop here, otherwise the dump could still be
                // created for the wrong height. The new tip could also not be
                // the target block if we have a stale sister block of
                // invalidate_index. This block (or a descendant) would be
                // activated as the new tip and we would not get to
                // new_tip_index.
                if (target_index != chainstate->m_chain.Tip()) {
                    LogPrintLevel(BCLog::RPC, BCLog::Level::Warning,
                                  "Failed to roll back to requested height, "
                                  "reverting to tip.\n");
                    throw JSONRPCError(
                        RPC_MISC_ERROR,
                        "Could not roll back to requested height.");
                } else {
                    std::tie(cursor, stats, tip) = PrepareUTXOSnapshot(
                        *chainstate, node.rpc_interruption_point);
                }
            }

            UniValue result =
                WriteUTXOSnapshot(*chainstate, cursor.get(), &stats, tip, afile,
                                  path, temppath, node.rpc_interruption_point);
            fs::rename(temppath, path);

            return result;
        },
    };
}

std::tuple<std::unique_ptr<CCoinsViewCursor>, CCoinsStats, const CBlockIndex *>
PrepareUTXOSnapshot(Chainstate &chainstate,
                    const std::function<void()> &interruption_point) {
    std::unique_ptr<CCoinsViewCursor> pcursor;
    std::optional<CCoinsStats> maybe_stats;
    const CBlockIndex *tip;

    {
        // We need to lock cs_main to ensure that the coinsdb isn't
        // written to between (i) flushing coins cache to disk
        // (coinsdb), (ii) getting stats based upon the coinsdb, and
        // (iii) constructing a cursor to the coinsdb for use in
        // WriteUTXOSnapshot.
        //
        // Cursors returned by leveldb iterate over snapshots, so the
        // contents of the pcursor will not be affected by simultaneous
        // writes during use below this block.
        //
        // See discussion here:
        //   https://github.com/bitcoin/bitcoin/pull/15606#discussion_r274479369
        //
        AssertLockHeld(::cs_main);

        chainstate.ForceFlushStateToDisk();

        maybe_stats = GetUTXOStats(&chainstate.CoinsDB(), chainstate.m_blockman,
                                   CoinStatsHashType::HASH_SERIALIZED,
                                   interruption_point);
        if (!maybe_stats) {
            throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
        }

        pcursor =
            std::unique_ptr<CCoinsViewCursor>(chainstate.CoinsDB().Cursor());
        tip = CHECK_NONFATAL(
            chainstate.m_blockman.LookupBlockIndex(maybe_stats->hashBlock));
    }

    return {std::move(pcursor), *CHECK_NONFATAL(maybe_stats), tip};
}

UniValue WriteUTXOSnapshot(Chainstate &chainstate, CCoinsViewCursor *pcursor,
                           CCoinsStats *maybe_stats, const CBlockIndex *tip,
                           AutoFile &afile, const fs::path &path,
                           const fs::path &temppath,
                           const std::function<void()> &interruption_point) {
    LOG_TIME_SECONDS(
        strprintf("writing UTXO snapshot at height %s (%s) to file %s (via %s)",
                  tip->nHeight, tip->GetBlockHash().ToString(),
                  fs::PathToString(path), fs::PathToString(temppath)));

    SnapshotMetadata metadata{tip->GetBlockHash(), maybe_stats->coins_count};

    afile << metadata;

    COutPoint key;
    TxId last_txid;
    Coin coin;
    unsigned int iter{0};
    size_t written_coins_count{0};
    std::vector<std::pair<uint32_t, Coin>> coins;

    // To reduce space the serialization format of the snapshot avoids
    // duplication of tx hashes. The code takes advantage of the guarantee by
    // leveldb that keys are lexicographically sorted.
    // In the coins vector we collect all coins that belong to a certain tx hash
    // (key.hash) and when we have them all (key.hash != last_hash) we write
    // them to file using the below lambda function.
    // See also https://github.com/bitcoin/bitcoin/issues/25675
    auto write_coins_to_file =
        [&](AutoFile &afile, const TxId &last_txid,
            const std::vector<std::pair<uint32_t, Coin>> &coins,
            size_t &written_coins_count) {
            afile << last_txid;
            WriteCompactSize(afile, coins.size());
            for (const auto &[n, coin_] : coins) {
                WriteCompactSize(afile, n);
                afile << coin_;
                ++written_coins_count;
            }
        };

    pcursor->GetKey(key);
    last_txid = key.GetTxId();
    while (pcursor->Valid()) {
        if (iter % 5000 == 0) {
            interruption_point();
        }
        ++iter;
        if (pcursor->GetKey(key) && pcursor->GetValue(coin)) {
            if (key.GetTxId() != last_txid) {
                write_coins_to_file(afile, last_txid, coins,
                                    written_coins_count);
                last_txid = key.GetTxId();
                coins.clear();
            }
            coins.emplace_back(key.GetN(), coin);
        }
        pcursor->Next();
    }

    if (!coins.empty()) {
        write_coins_to_file(afile, last_txid, coins, written_coins_count);
    }

    CHECK_NONFATAL(written_coins_count == maybe_stats->coins_count);

    afile.fclose();

    UniValue result(UniValue::VOBJ);
    result.pushKV("coins_written", written_coins_count);
    result.pushKV("base_hash", tip->GetBlockHash().ToString());
    result.pushKV("base_height", tip->nHeight);
    result.pushKV("path", path.u8string());
    result.pushKV("txoutset_hash", maybe_stats->hashSerialized.ToString());
    result.pushKV("nchaintx", tip->nChainTx);
    return result;
}

UniValue CreateUTXOSnapshot(node::NodeContext &node, Chainstate &chainstate,
                            AutoFile &afile, const fs::path &path,
                            const fs::path &tmppath) {
    auto [cursor, stats, tip]{WITH_LOCK(
        ::cs_main,
        return PrepareUTXOSnapshot(chainstate, node.rpc_interruption_point))};
    return WriteUTXOSnapshot(chainstate, cursor.get(), &stats, tip, afile, path,
                             tmppath, node.rpc_interruption_point);
}

static RPCHelpMan loadtxoutset() {
    return RPCHelpMan{
        "loadtxoutset",
        "Load the serialized UTXO set from a file.\n"
        "Once this snapshot is loaded, its contents will be deserialized into "
        "a second chainstate data structure, which is then used to sync to the "
        "network's tip. "
        "Meanwhile, the original chainstate will complete the initial block "
        "download process in the background, eventually validating up to the "
        "block that the snapshot is based upon.\n\n"
        "The result is a usable bitcoind instance that is current with the "
        "network tip in a matter of minutes rather than hours. UTXO snapshot "
        "are typically obtained from third-party sources (HTTP, torrent, etc.) "
        "which is reasonable since their contents are always checked by "
        "hash.\n\n"
        "This RPC is incompatible with the -chronik init option, and a node "
        "with multiple chainstates may not be restarted with -chronik. After "
        "the background validation is finished and the chainstates are merged, "
        "the node can be restarted again with Chronik.\n\n"
        "You can find more information on this process in the `assumeutxo` "
        "design document (https://www.bitcoinabc.org/doc/assumeutxo.html).",
        {
            {"path", RPCArg::Type::STR, RPCArg::Optional::NO,
             "path to the snapshot file. If relative, will be prefixed by "
             "datadir."},
        },
        RPCResult{RPCResult::Type::OBJ,
                  "",
                  "",
                  {
                      {RPCResult::Type::NUM, "coins_loaded",
                       "the number of coins loaded from the snapshot"},
                      {RPCResult::Type::STR_HEX, "tip_hash",
                       "the hash of the base of the snapshot"},
                      {RPCResult::Type::NUM, "base_height",
                       "the height of the base of the snapshot"},
                      {RPCResult::Type::STR, "path",
                       "the absolute path that the snapshot was loaded from"},
                  }},
        RPCExamples{
            HelpExampleCli("loadtxoutset -rpcclienttimeout=0", "utxo.dat")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            NodeContext &node = EnsureAnyNodeContext(request.context);
            ChainstateManager &chainman = EnsureChainman(node);
            ArgsManager &args = EnsureArgsman(node);
            const fs::path path{AbsPathForConfigVal(
                args, fs::u8path(self.Arg<std::string>("path")))};

            if (args.GetBoolArg("-chronik", false)) {
                throw JSONRPCError(
                    RPC_MISC_ERROR,
                    "loadtxoutset is not compatible with Chronik.");
            }

            FILE *file{fsbridge::fopen(path, "rb")};
            AutoFile afile{file};
            if (afile.IsNull()) {
                throw JSONRPCError(RPC_INVALID_PARAMETER,
                                   "Couldn't open file " + path.u8string() +
                                       " for reading.");
            }

            SnapshotMetadata metadata;
            try {
                afile >> metadata;
            } catch (const std::ios_base::failure &e) {
                throw JSONRPCError(
                    RPC_DESERIALIZATION_ERROR,
                    strprintf("Unable to parse metadata: %s", e.what()));
            }

            auto activation_result{
                chainman.ActivateSnapshot(afile, metadata, false)};
            if (!activation_result) {
                throw JSONRPCError(
                    RPC_INTERNAL_ERROR,
                    strprintf("Unable to load UTXO snapshot: %s. (%s)",
                              util::ErrorString(activation_result).original,
                              path.u8string()));
            }

            CBlockIndex &snapshot_index{*CHECK_NONFATAL(*activation_result)};

            // Because we can't provide historical blocks during tip or
            // background sync. Update local services to reflect we are a
            // limited peer until we are fully sync.
            node.connman->RemoveLocalServices(NODE_NETWORK);
            // Setting the limited state is usually redundant because the node
            // can always provide the last 288 blocks, but it doesn't hurt to
            // set it.
            node.connman->AddLocalServices(NODE_NETWORK_LIMITED);

            UniValue result(UniValue::VOBJ);
            result.pushKV("coins_loaded", metadata.m_coins_count);
            result.pushKV("tip_hash", snapshot_index.GetBlockHash().ToString());
            result.pushKV("base_height", snapshot_index.nHeight);
            result.pushKV("path", fs::PathToString(path));
            return result;
        },
    };
}

const std::vector<RPCResult> RPCHelpForChainstate{
    {RPCResult::Type::NUM, "blocks", "number of blocks in this chainstate"},
    {RPCResult::Type::STR_HEX, "bestblockhash", "blockhash of the tip"},
    {RPCResult::Type::NUM, "difficulty", "difficulty of the tip"},
    {RPCResult::Type::NUM, "verificationprogress",
     "progress towards the network tip"},
    {RPCResult::Type::STR_HEX, "snapshot_blockhash", /*optional=*/true,
     "the base block of the snapshot this chainstate is based on, if any"},
    {RPCResult::Type::NUM, "coins_db_cache_bytes", "size of the coinsdb cache"},
    {RPCResult::Type::NUM, "coins_tip_cache_bytes",
     "size of the coinstip cache"},
    {RPCResult::Type::BOOL, "validated",
     "whether the chainstate is fully validated. True if all blocks in the "
     "chainstate were validated, false if the chain is based on a snapshot and "
     "the snapshot has not yet been validated."},

};

static RPCHelpMan getchainstates() {
    return RPCHelpMan{
        "getchainstates",
        "\nReturn information about chainstates.\n",
        {},
        RPCResult{RPCResult::Type::OBJ,
                  "",
                  "",
                  {
                      {RPCResult::Type::NUM, "headers",
                       "the number of headers seen so far"},
                      {RPCResult::Type::ARR,
                       "chainstates",
                       "list of the chainstates ordered by work, with the "
                       "most-work (active) chainstate last",
                       {
                           {RPCResult::Type::OBJ, "", "", RPCHelpForChainstate},
                       }},
                  }},
        RPCExamples{HelpExampleCli("getchainstates", "") +
                    HelpExampleRpc("getchainstates", "")},
        [&](const RPCHelpMan &self, const Config &config,
            const JSONRPCRequest &request) -> UniValue {
            LOCK(cs_main);
            UniValue obj(UniValue::VOBJ);

            ChainstateManager &chainman = EnsureAnyChainman(request.context);

            auto make_chain_data =
                [&](const Chainstate &chainstate,
                    bool validated) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
                    AssertLockHeld(::cs_main);
                    UniValue data(UniValue::VOBJ);
                    if (!chainstate.m_chain.Tip()) {
                        return data;
                    }
                    const CChain &chain = chainstate.m_chain;
                    const CBlockIndex *tip = chain.Tip();

                    data.pushKV("blocks", chain.Height());
                    data.pushKV("bestblockhash", tip->GetBlockHash().GetHex());
                    data.pushKV("difficulty", GetDifficulty(*tip));
                    data.pushKV(
                        "verificationprogress",
                        GuessVerificationProgress(Params().TxData(), tip));
                    data.pushKV("coins_db_cache_bytes",
                                chainstate.m_coinsdb_cache_size_bytes);
                    data.pushKV("coins_tip_cache_bytes",
                                chainstate.m_coinstip_cache_size_bytes);
                    if (chainstate.m_from_snapshot_blockhash) {
                        data.pushKV(
                            "snapshot_blockhash",
                            chainstate.m_from_snapshot_blockhash->ToString());
                    }
                    data.pushKV("validated", validated);
                    return data;
                };

            obj.pushKV("headers", chainman.m_best_header
                                      ? chainman.m_best_header->nHeight
                                      : -1);

            const auto &chainstates = chainman.GetAll();
            UniValue obj_chainstates{UniValue::VARR};
            for (Chainstate *cs : chainstates) {
                obj_chainstates.push_back(
                    make_chain_data(*cs, !cs->m_from_snapshot_blockhash ||
                                             chainstates.size() == 1));
            }
            obj.pushKV("chainstates", std::move(obj_chainstates));
            return obj;
        }};
}

void RegisterBlockchainRPCCommands(CRPCTable &t) {
    // clang-format off
    static const CRPCCommand commands[] = {
        //  category            actor (function)
        //  ------------------  ----------------------
        { "blockchain",         getbestblockhash,                  },
        { "blockchain",         getblock,                          },
        { "blockchain",         getblockfrompeer,                  },
        { "blockchain",         getblockchaininfo,                 },
        { "blockchain",         getblockcount,                     },
        { "blockchain",         getblockhash,                      },
        { "blockchain",         getblockheader,                    },
        { "blockchain",         getblockstats,                     },
        { "blockchain",         getchaintips,                      },
        { "blockchain",         getchaintxstats,                   },
        { "blockchain",         getdifficulty,                     },
        { "blockchain",         gettxout,                          },
        { "blockchain",         gettxoutsetinfo,                   },
        { "blockchain",         pruneblockchain,                   },
        { "blockchain",         verifychain,                       },
        { "blockchain",         preciousblock,                     },
        { "blockchain",         scantxoutset,                      },
        { "blockchain",         getblockfilter,                    },
        { "blockchain",         dumptxoutset,                      },
        { "blockchain",         loadtxoutset,                      },
        { "blockchain",         getchainstates,                    },

        /* Not shown in help */
        { "hidden",             invalidateblock,                   },
        { "hidden",             parkblock,                         },
        { "hidden",             reconsiderblock,                   },
        { "hidden",             syncwithvalidationinterfacequeue,  },
        { "hidden",             unparkblock,                       },
        { "hidden",             waitfornewblock,                   },
        { "hidden",             waitforblock,                      },
        { "hidden",             waitforblockheight,                },
    };
    // clang-format on
    for (const auto &c : commands) {
        t.appendCommand(c.name, &c);
    }
}
